mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 594ed85a71 |
@@ -4,7 +4,7 @@ WG Tunnel
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
An alternative Android client app for [WireGuard](https://www.wireguard.com/)
|
An alternative Android client app for [WireGuard®](https://www.wireguard.com/)
|
||||||
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
|
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
@@ -23,14 +23,14 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
|
|||||||
[](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
|
[](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
|
||||||
[](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
|
[](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
|
||||||
[](https://github.com/zaneschepke/fdroid)
|
[](https://github.com/zaneschepke/fdroid)
|
||||||
[](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22com.zaneschepke.wireguardautotunnel%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fzaneschepke%2Fwgtunnel%22%2C%22author%22%3A%22zaneschepke%22%2C%22name%22%3A%22WG%20Tunnel%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Atrue%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22WG%20Tunnel%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22Zane%20Schepke%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22overrideSource%22%3Anull%7D)
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[<img src="https://img.shields.io/badge/Telegram-26A5E4.svg?style=for-the-badge&logo=Telegram&logoColor=white">](https://t.me/wgtunnel)
|
[](https://discord.gg/rbRRNh6H7V)
|
||||||
[<img src="https://img.shields.io/badge/Matrix-000000.svg?style=for-the-badge&logo=Matrix&logoColor=white">](https://matrix.to/#/#wg-tunnel-space:matrix.org)
|
[](https://t.me/wgtunnel)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details open="open">
|
<details open="open">
|
||||||
@@ -49,7 +49,7 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
|
|||||||
<div style="text-align: left;">
|
<div style="text-align: left;">
|
||||||
|
|
||||||
## About
|
## About
|
||||||
Inspired by the official [wireguard-android](https://github.com/WireGuard/wireguard-android) app, WG Tunnel was created to address features and support missing from the official app. This app combines support for both [WireGuard](https://www.wireguard.com/)
|
Inspired by the official [wireguard-android](https://github.com/WireGuard/wireguard-android) app, WG Tunnel was created to address features and support missing from the official app. This app combines support for both [WireGuard®](https://www.wireguard.com/)
|
||||||
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/), with its primary feature of auto-tunneling (on-demand tunneling).
|
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/), with its primary feature of auto-tunneling (on-demand tunneling).
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -61,14 +61,14 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/), with its pr
|
|||||||
Thank you to the following:
|
Thank you to the following:
|
||||||
|
|
||||||
- All of the users that have helped contribute to the project with ideas, translations, feedback, bug reports, testing, and donations.
|
- All of the users that have helped contribute to the project with ideas, translations, feedback, bug reports, testing, and donations.
|
||||||
- [WireGuard](https://www.wireguard.com/) - Jason A. Donenfeld (https://github.com/WireGuard/wireguard-android)
|
- [WireGuard®](https://www.wireguard.com/) - © Jason A. Donenfeld (https://github.com/WireGuard/wireguard-android)
|
||||||
|
|
||||||
- [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) - Amnezia Team (https://github.com/amnezia-vpn/amneziawg-android)
|
- [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) - Amnezia Team (https://github.com/amnezia-vpn/amneziawg-android)
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; flex-wrap: wrap; justify-content: left; gap: 10px;">
|
<div style="display: flex; flex-wrap: wrap; justify-content: center; gap: 10px;">
|
||||||
<img label="Main" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" />
|
<img label="Main" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" />
|
||||||
<img label="Settings" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" />
|
<img label="Settings" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" />
|
||||||
<img label="Auto" src="fastlane/metadata/android/en-US/images/phoneScreenshots/auto_screen.png" width="200" />
|
<img label="Auto" src="fastlane/metadata/android/en-US/images/phoneScreenshots/auto_screen.png" width="200" />
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ android {
|
|||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
applicationIdSuffix = ".debug"
|
applicationIdSuffix = ".debug"
|
||||||
|
versionNameSuffix = "-debug"
|
||||||
resValue("string", "app_name", "WG Tunnel - Debug")
|
resValue("string", "app_name", "WG Tunnel - Debug")
|
||||||
isDebuggable = true
|
isDebuggable = true
|
||||||
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.debug\"")
|
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.debug\"")
|
||||||
@@ -94,6 +95,7 @@ android {
|
|||||||
create(Constants.PRERELEASE) {
|
create(Constants.PRERELEASE) {
|
||||||
initWith(buildTypes.getByName(Constants.RELEASE))
|
initWith(buildTypes.getByName(Constants.RELEASE))
|
||||||
applicationIdSuffix = ".prerelease"
|
applicationIdSuffix = ".prerelease"
|
||||||
|
versionNameSuffix = "-pre"
|
||||||
resValue("string", "app_name", "WG Tunnel - Pre")
|
resValue("string", "app_name", "WG Tunnel - Pre")
|
||||||
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.pre\"")
|
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.pre\"")
|
||||||
}
|
}
|
||||||
@@ -101,6 +103,7 @@ android {
|
|||||||
create(Constants.NIGHTLY) {
|
create(Constants.NIGHTLY) {
|
||||||
initWith(buildTypes.getByName(Constants.RELEASE))
|
initWith(buildTypes.getByName(Constants.RELEASE))
|
||||||
applicationIdSuffix = ".nightly"
|
applicationIdSuffix = ".nightly"
|
||||||
|
versionNameSuffix = "-nightly"
|
||||||
resValue("string", "app_name", "WG Tunnel - Nightly")
|
resValue("string", "app_name", "WG Tunnel - Nightly")
|
||||||
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.nightly\"")
|
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.nightly\"")
|
||||||
}
|
}
|
||||||
@@ -141,8 +144,8 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
implementation(project(":logcatter"))
|
implementation(project(":logcatter"))
|
||||||
implementation(project(":networkmonitor"))
|
|
||||||
|
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
@@ -157,7 +160,6 @@ dependencies {
|
|||||||
implementation(libs.androidx.material3)
|
implementation(libs.androidx.material3)
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
implementation(libs.material)
|
implementation(libs.material)
|
||||||
implementation(libs.androidx.storage)
|
|
||||||
|
|
||||||
// test
|
// test
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
@@ -202,12 +204,13 @@ dependencies {
|
|||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.lifecycle.process)
|
implementation(libs.androidx.lifecycle.process)
|
||||||
|
|
||||||
|
// icons
|
||||||
|
implementation(libs.material.icons.extended)
|
||||||
// serialization
|
// serialization
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
|
||||||
// ui
|
// barcode scanning
|
||||||
implementation(libs.zxing.android.embedded)
|
implementation(libs.zxing.android.embedded)
|
||||||
implementation(libs.material.icons.extended)
|
|
||||||
|
|
||||||
// bio
|
// bio
|
||||||
implementation(libs.androidx.biometric.ktx)
|
implementation(libs.androidx.biometric.ktx)
|
||||||
|
|||||||
@@ -3,40 +3,3 @@
|
|||||||
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||||
<fields>;
|
<fields>;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Keep all classes in the org.xbill.DNS package and subpackages
|
|
||||||
-keep class org.xbill.DNS.** { *; }
|
|
||||||
-dontwarn org.xbill.DNS.**
|
|
||||||
|
|
||||||
# Preserve JNA classes if used (e.g., for IPHlpAPI on Windows)
|
|
||||||
-keep class com.sun.jna.** { *; }
|
|
||||||
-dontwarn com.sun.jna.**
|
|
||||||
|
|
||||||
# Keep DNS resolver configuration classes that might be loaded dynamically
|
|
||||||
-keep class org.xbill.DNS.config.** { *; }
|
|
||||||
-dontwarn org.xbill.DNS.config.**
|
|
||||||
|
|
||||||
-keep class org.xbill.DNS.** { *; }
|
|
||||||
|
|
||||||
# Prevent optimization issues with native or reflection-based calls
|
|
||||||
-dontoptimize
|
|
||||||
-dontshrink
|
|
||||||
# Uncomment the above if errors persist, but use sparingly as they’re broad
|
|
||||||
|
|
||||||
# Suppress warnings about missing classes if not all features are used
|
|
||||||
-dontwarn java.lang.management.**
|
|
||||||
-dontwarn sun.nio.ch.**
|
|
||||||
|
|
||||||
-dontwarn com.google.api.client.http.GenericUrl
|
|
||||||
-dontwarn com.google.api.client.http.HttpHeaders
|
|
||||||
-dontwarn com.google.api.client.http.HttpRequest
|
|
||||||
-dontwarn com.google.api.client.http.HttpRequestFactory
|
|
||||||
-dontwarn com.google.api.client.http.HttpResponse
|
|
||||||
-dontwarn com.google.api.client.http.HttpTransport
|
|
||||||
-dontwarn com.google.api.client.http.javanet.NetHttpTransport$Builder
|
|
||||||
-dontwarn com.google.api.client.http.javanet.NetHttpTransport
|
|
||||||
-dontwarn javax.lang.model.element.Modifier
|
|
||||||
-dontwarn org.joda.time.Instant
|
|
||||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
|
||||||
-dontwarn org.slf4j.impl.StaticMDCBinder
|
|
||||||
-dontwarn org.slf4j.impl.StaticMarkerBinder
|
|
||||||
Vendored
-37
@@ -22,40 +22,3 @@
|
|||||||
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||||
<fields>;
|
<fields>;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Keep all classes in the org.xbill.DNS package and subpackages
|
|
||||||
-keep class org.xbill.DNS.** { *; }
|
|
||||||
-dontwarn org.xbill.DNS.**
|
|
||||||
|
|
||||||
# Preserve JNA classes if used (e.g., for IPHlpAPI on Windows)
|
|
||||||
-keep class com.sun.jna.** { *; }
|
|
||||||
-dontwarn com.sun.jna.**
|
|
||||||
|
|
||||||
# Keep DNS resolver configuration classes that might be loaded dynamically
|
|
||||||
-keep class org.xbill.DNS.config.** { *; }
|
|
||||||
-dontwarn org.xbill.DNS.config.**
|
|
||||||
|
|
||||||
-keep class org.xbill.DNS.** { *; }
|
|
||||||
|
|
||||||
# Prevent optimization issues with native or reflection-based calls
|
|
||||||
-dontoptimize
|
|
||||||
-dontshrink
|
|
||||||
# Uncomment the above if errors persist, but use sparingly as they’re broad
|
|
||||||
|
|
||||||
# Suppress warnings about missing classes if not all features are used
|
|
||||||
-dontwarn java.lang.management.**
|
|
||||||
-dontwarn sun.nio.ch.**
|
|
||||||
|
|
||||||
-dontwarn com.google.api.client.http.GenericUrl
|
|
||||||
-dontwarn com.google.api.client.http.HttpHeaders
|
|
||||||
-dontwarn com.google.api.client.http.HttpRequest
|
|
||||||
-dontwarn com.google.api.client.http.HttpRequestFactory
|
|
||||||
-dontwarn com.google.api.client.http.HttpResponse
|
|
||||||
-dontwarn com.google.api.client.http.HttpTransport
|
|
||||||
-dontwarn com.google.api.client.http.javanet.NetHttpTransport$Builder
|
|
||||||
-dontwarn com.google.api.client.http.javanet.NetHttpTransport
|
|
||||||
-dontwarn javax.lang.model.element.Modifier
|
|
||||||
-dontwarn org.joda.time.Instant
|
|
||||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
|
||||||
-dontwarn org.slf4j.impl.StaticMDCBinder
|
|
||||||
-dontwarn org.slf4j.impl.StaticMarkerBinder
|
|
||||||
@@ -3,8 +3,13 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||||
<!--foreground service exempt android 14-->
|
<!--foreground service exempt android 14-->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"
|
||||||
@@ -63,7 +68,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:windowSoftInputMode="adjustNothing"
|
android:windowSoftInputMode="adjustResize"
|
||||||
android:theme="@style/Theme.WireguardAutoTunnel"
|
android:theme="@style/Theme.WireguardAutoTunnel"
|
||||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||||
>
|
>
|
||||||
@@ -163,14 +168,22 @@
|
|||||||
</service>
|
</service>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".core.broadcast.RestartReceiver"
|
android:name=".core.broadcast.BootReceiver"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="true">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
|
||||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
<receiver
|
||||||
|
android:name=".core.broadcast.AppUpdateReceiver"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|||||||
@@ -1,35 +1,23 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel
|
package com.zaneschepke.wireguardautotunnel
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings
|
|
||||||
import androidx.activity.SystemBarStyle
|
import androidx.activity.SystemBarStyle
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.result.ActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.imePadding
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.Bolt
|
|
||||||
import androidx.compose.material.icons.rounded.Home
|
import androidx.compose.material.icons.rounded.Home
|
||||||
import androidx.compose.material.icons.rounded.QuestionMark
|
import androidx.compose.material.icons.rounded.QuestionMark
|
||||||
import androidx.compose.material.icons.rounded.Settings
|
import androidx.compose.material.icons.rounded.Settings
|
||||||
@@ -37,62 +25,51 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarData
|
import androidx.compose.material3.SnackbarData
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
|
||||||
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
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import androidx.navigation.toRoute
|
import androidx.navigation.toRoute
|
||||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.CustomBottomNavbar
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.DynamicTopAppBar
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.currentNavBackStackEntryAsNavBarState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarControllerProvider
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.TunnelAutoTunnelScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.OptionsScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.ConfigScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.PinLockScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.scanner.ScannerScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ScannerScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.SplitTunnelScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.SplitTunnelScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.TunnelOptionsScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.TunnelAutoTunnelScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.pin.PinLockScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.AdvancedScreen
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.AppearanceScreen
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.DisplayScreen
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.KillSwitchScreen
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.LanguageScreen
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.LocationDisclosureScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.AppearanceScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.AutoTunnelScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display.DisplayScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.support.LogsScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.AutoTunnelScreen
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced.AdvancedScreen
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.disclosure.LocationDisclosureScreen
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.KillSwitchScreen
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.goFromRoot
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.amnezia.awg.backend.GoBackend.VpnService
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
@@ -106,11 +83,8 @@ class MainActivity : AppCompatActivity() {
|
|||||||
lateinit var tunnelManager: TunnelManager
|
lateinit var tunnelManager: TunnelManager
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var networkMonitor: NetworkMonitor
|
lateinit var shortcutManager: ShortcutManager
|
||||||
|
|
||||||
private var lastLocationPermissionState: Boolean? = null
|
|
||||||
|
|
||||||
@SuppressLint("BatteryLife")
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
enableEdgeToEdge(
|
enableEdgeToEdge(
|
||||||
statusBarStyle = SystemBarStyle.Companion.auto(Color.TRANSPARENT, Color.TRANSPARENT),
|
statusBarStyle = SystemBarStyle.Companion.auto(Color.TRANSPARENT, Color.TRANSPARENT),
|
||||||
@@ -125,112 +99,51 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
installSplashScreen().apply {
|
installSplashScreen().apply {
|
||||||
setKeepOnScreenCondition {
|
setKeepOnScreenCondition {
|
||||||
!viewModel.appViewState.value.isAppReady
|
!viewModel.isAppReady.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val appUiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val appUiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val appViewState by viewModel.appViewState.collectAsStateWithLifecycle()
|
val configurationChange by viewModel.configurationChange.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val backStackEntry by navController.currentBackStackEntryAsState()
|
|
||||||
val navBarState by currentNavBackStackEntryAsNavBarState(navController, backStackEntry, viewModel, appUiState)
|
|
||||||
val snackbar = remember { SnackbarHostState() }
|
|
||||||
var showVpnPermissionDialog by remember { mutableStateOf(false) }
|
|
||||||
var vpnPermissionDenied by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val vpnActivity =
|
LaunchedEffect(configurationChange) {
|
||||||
rememberLauncherForActivityResult(
|
if (configurationChange) {
|
||||||
ActivityResultContracts.StartActivityForResult(),
|
|
||||||
onResult = {
|
|
||||||
if (it.resultCode != RESULT_OK) {
|
|
||||||
showVpnPermissionDialog = true
|
|
||||||
vpnPermissionDenied = true
|
|
||||||
} else {
|
|
||||||
vpnPermissionDenied = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
LaunchedEffect(appUiState.tunnels) {
|
|
||||||
if (!appViewState.isAppReady) {
|
|
||||||
viewModel.handleEvent(AppEvent.AppReadyCheck(appUiState.tunnels))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val batteryActivity = rememberLauncherForActivityResult(
|
|
||||||
ActivityResultContracts.StartActivityForResult(),
|
|
||||||
) { _: ActivityResult ->
|
|
||||||
viewModel.handleEvent(AppEvent.SetBatteryOptimizeDisableShown)
|
|
||||||
}
|
|
||||||
|
|
||||||
with(appViewState) {
|
|
||||||
LaunchedEffect(isConfigChanged) {
|
|
||||||
if (isConfigChanged) {
|
|
||||||
Intent(this@MainActivity, MainActivity::class.java).also {
|
Intent(this@MainActivity, MainActivity::class.java).also {
|
||||||
startActivity(it)
|
startActivity(it)
|
||||||
exitProcess(0)
|
exitProcess(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LaunchedEffect(errorMessage) {
|
|
||||||
errorMessage?.let {
|
LaunchedEffect(Unit) {
|
||||||
snackbar.showSnackbar(it.asString(this@MainActivity))
|
viewModel.getEmitSplitTunnelApps(this@MainActivity)
|
||||||
viewModel.handleEvent(AppEvent.MessageShown)
|
}
|
||||||
}
|
|
||||||
}
|
LaunchedEffect(appUiState.autoTunnelActive) {
|
||||||
LaunchedEffect(appUiState.activeTunnels) {
|
requestAutoTunnelTileServiceUpdate()
|
||||||
appUiState.activeTunnels
|
}
|
||||||
.mapNotNull { (tunnelConf, tunnelState) ->
|
|
||||||
(tunnelState.status as? TunnelStatus.Error)?.let { error ->
|
with(appUiState.appSettings) {
|
||||||
val message = error.error.toStringRes()
|
LaunchedEffect(isAutoTunnelEnabled) {
|
||||||
val context = this@MainActivity
|
this@MainActivity.requestAutoTunnelTileServiceUpdate()
|
||||||
snackbar.showSnackbar(context.getString(R.string.tunnel_error_template, context.getString(message)))
|
}
|
||||||
viewModel.handleEvent(AppEvent.ClearTunnelError(tunnelConf))
|
LaunchedEffect(isShortcutsEnabled) {
|
||||||
}
|
if (!isShortcutsEnabled) return@LaunchedEffect shortcutManager.removeShortcuts()
|
||||||
}
|
shortcutManager.addShortcuts()
|
||||||
}
|
|
||||||
LaunchedEffect(popBackStack) {
|
|
||||||
if (popBackStack) {
|
|
||||||
navController.popBackStack()
|
|
||||||
viewModel.handleEvent(AppEvent.PopBackStack(false))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LaunchedEffect(requestVpnPermission) {
|
|
||||||
if (requestVpnPermission) {
|
|
||||||
if (!vpnPermissionDenied) {
|
|
||||||
vpnActivity.launch(VpnService.prepare(this@MainActivity))
|
|
||||||
} else {
|
|
||||||
showVpnPermissionDialog = true
|
|
||||||
}
|
|
||||||
viewModel.handleEvent(AppEvent.VpnPermissionRequested)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LaunchedEffect(requestBatteryPermission) {
|
|
||||||
if (requestBatteryPermission) {
|
|
||||||
batteryActivity.launch(
|
|
||||||
Intent().apply {
|
|
||||||
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
|
||||||
data = Uri.parse("package:${this@MainActivity.packageName}")
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ServiceWorker.start(this)
|
||||||
|
|
||||||
CompositionLocalProvider(LocalNavController provides navController) {
|
CompositionLocalProvider(LocalNavController provides navController) {
|
||||||
|
SnackbarControllerProvider { host ->
|
||||||
WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) {
|
WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) {
|
||||||
VpnDeniedDialog(showVpnPermissionDialog, onDismiss = { showVpnPermissionDialog = false })
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.pointerInput(Unit) {
|
contentWindowInsets = WindowInsets(0),
|
||||||
detectTapGestures {
|
|
||||||
viewModel.handleEvent(AppEvent.SetSelectedTunnel(null))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
snackbarHost = {
|
snackbarHost = {
|
||||||
SnackbarHost(snackbar) { snackbarData: SnackbarData ->
|
SnackbarHost(host) { snackbarData: SnackbarData ->
|
||||||
CustomSnackBar(
|
CustomSnackBar(
|
||||||
snackbarData.visuals.message,
|
snackbarData.visuals.message,
|
||||||
isRtl = false,
|
isRtl = false,
|
||||||
@@ -241,72 +154,54 @@ class MainActivity : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
topBar = {
|
|
||||||
DynamicTopAppBar(navBarState)
|
|
||||||
},
|
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
AnimatedVisibility(
|
BottomNavBar(
|
||||||
visible = navBarState.showBottom,
|
navController,
|
||||||
enter = slideInVertically(initialOffsetY = { it }),
|
|
||||||
exit = slideOutVertically(targetOffsetY = { it }),
|
|
||||||
) {
|
|
||||||
CustomBottomNavbar(
|
|
||||||
listOf(
|
listOf(
|
||||||
BottomNavItem(
|
BottomNavItem(
|
||||||
name = stringResource(R.string.tunnels),
|
name = stringResource(R.string.tunnels),
|
||||||
route = Route.Main,
|
route = Route.Main,
|
||||||
icon = Icons.Rounded.Home,
|
icon = Icons.Rounded.Home,
|
||||||
onClick = { navController.goFromRoot(Route.Main) },
|
|
||||||
),
|
|
||||||
BottomNavItem(
|
|
||||||
name = stringResource(R.string.auto_tunnel),
|
|
||||||
route = Route.AutoTunnel,
|
|
||||||
icon = Icons.Rounded.Bolt,
|
|
||||||
onClick = {
|
|
||||||
val route = if (appUiState.generalState.isLocationDisclosureShown) Route.AutoTunnel else Route.LocationDisclosure
|
|
||||||
navController.goFromRoot(route)
|
|
||||||
},
|
|
||||||
active = appUiState.isAutoTunnelActive,
|
|
||||||
),
|
),
|
||||||
BottomNavItem(
|
BottomNavItem(
|
||||||
name = stringResource(R.string.settings),
|
name = stringResource(R.string.settings),
|
||||||
route = Route.Settings,
|
route = Route.Settings,
|
||||||
icon = Icons.Rounded.Settings,
|
icon = Icons.Rounded.Settings,
|
||||||
onClick = { navController.goFromRoot(Route.Settings) },
|
|
||||||
),
|
),
|
||||||
BottomNavItem(
|
BottomNavItem(
|
||||||
name = stringResource(R.string.support),
|
name = stringResource(R.string.support),
|
||||||
route = Route.Support,
|
route = Route.Support,
|
||||||
icon = Icons.Rounded.QuestionMark,
|
icon = Icons.Rounded.QuestionMark,
|
||||||
onClick = { navController.goFromRoot(Route.Support) },
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
navBarState = navBarState,
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Box(
|
Box(modifier = Modifier.Companion.fillMaxSize().padding(padding)) {
|
||||||
modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.surface)
|
|
||||||
.padding(padding)
|
|
||||||
.consumeWindowInsets(padding)
|
|
||||||
.imePadding(),
|
|
||||||
) {
|
|
||||||
NavHost(
|
NavHost(
|
||||||
navController,
|
navController,
|
||||||
|
enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) },
|
||||||
|
exitTransition = { fadeOut(tween(Constants.TRANSITION_ANIMATION_TIME)) },
|
||||||
startDestination = (if (appUiState.generalState.isPinLockEnabled) Route.Lock else Route.Main),
|
startDestination = (if (appUiState.generalState.isPinLockEnabled) Route.Lock else Route.Main),
|
||||||
) {
|
) {
|
||||||
composable<Route.Main> {
|
composable<Route.Main> {
|
||||||
MainScreen(appUiState, appViewState, viewModel)
|
MainScreen(
|
||||||
|
uiState = appUiState,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
composable<Route.Settings> {
|
composable<Route.Settings> {
|
||||||
SettingsScreen(appUiState, appViewState, viewModel)
|
SettingsScreen(
|
||||||
|
appViewModel = viewModel,
|
||||||
|
uiState = appUiState,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
composable<Route.LocationDisclosure> {
|
composable<Route.LocationDisclosure> {
|
||||||
LocationDisclosureScreen(appUiState, viewModel)
|
LocationDisclosureScreen(viewModel, appUiState)
|
||||||
}
|
}
|
||||||
composable<Route.AutoTunnel> {
|
composable<Route.AutoTunnel> {
|
||||||
AutoTunnelScreen(appUiState, viewModel)
|
AutoTunnelScreen(
|
||||||
|
appUiState.appSettings,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
composable<Route.Appearance> {
|
composable<Route.Appearance> {
|
||||||
AppearanceScreen()
|
AppearanceScreen()
|
||||||
@@ -315,51 +210,50 @@ class MainActivity : AppCompatActivity() {
|
|||||||
LanguageScreen(appUiState, viewModel)
|
LanguageScreen(appUiState, viewModel)
|
||||||
}
|
}
|
||||||
composable<Route.Display> {
|
composable<Route.Display> {
|
||||||
DisplayScreen(appUiState, viewModel)
|
DisplayScreen(appUiState)
|
||||||
}
|
}
|
||||||
composable<Route.Support> {
|
composable<Route.Support> {
|
||||||
SupportScreen()
|
SupportScreen(appUiState, viewModel)
|
||||||
}
|
}
|
||||||
composable<Route.AutoTunnelAdvanced> {
|
composable<Route.AutoTunnelAdvanced> {
|
||||||
AdvancedScreen(appUiState)
|
AdvancedScreen(appUiState.appSettings, viewModel)
|
||||||
}
|
}
|
||||||
composable<Route.Logs> {
|
composable<Route.Logs> {
|
||||||
LogsScreen(appViewState, viewModel)
|
LogsScreen()
|
||||||
}
|
}
|
||||||
composable<Route.Config> { backStack ->
|
composable<Route.Config> {
|
||||||
val args = backStack.toRoute<Route.Config>()
|
val args = it.toRoute<Route.Config>()
|
||||||
val config = appUiState.tunnels.firstOrNull { it.id == args.id }
|
val config =
|
||||||
|
appUiState.tunnels.firstOrNull { it.id == args.id }
|
||||||
ConfigScreen(config, viewModel)
|
ConfigScreen(config, viewModel)
|
||||||
}
|
}
|
||||||
composable<Route.TunnelOptions> { backStack ->
|
composable<Route.TunnelOptions> {
|
||||||
val args = backStack.toRoute<Route.TunnelOptions>()
|
val args = it.toRoute<Route.TunnelOptions>()
|
||||||
appUiState.tunnels.firstOrNull { it.id == args.id }?.let { config ->
|
val config = appUiState.tunnels.first { it.id == args.id }
|
||||||
TunnelOptionsScreen(config, appUiState, viewModel)
|
OptionsScreen(config)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
composable<Route.Lock> {
|
composable<Route.Lock> {
|
||||||
PinLockScreen(viewModel)
|
PinLockScreen(viewModel)
|
||||||
}
|
}
|
||||||
composable<Route.Scanner> {
|
composable<Route.Scanner> {
|
||||||
ScannerScreen(viewModel)
|
ScannerScreen()
|
||||||
}
|
}
|
||||||
composable<Route.KillSwitch> {
|
composable<Route.KillSwitch> {
|
||||||
KillSwitchScreen(appUiState, viewModel)
|
KillSwitchScreen(appUiState, viewModel)
|
||||||
}
|
}
|
||||||
composable<Route.SplitTunnel> {
|
composable<Route.SplitTunnel> {
|
||||||
SplitTunnelScreen(viewModel)
|
val args = it.toRoute<Route.SplitTunnel>()
|
||||||
}
|
val config = appUiState.tunnels.first { it.id == args.id }
|
||||||
composable<Route.TunnelAutoTunnel> { backStack ->
|
SplitTunnelScreen(config, viewModel)
|
||||||
val args = backStack.toRoute<Route.TunnelOptions>()
|
|
||||||
appUiState.tunnels.firstOrNull { it.id == args.id }?.let {
|
|
||||||
TunnelAutoTunnelScreen(it, appUiState.appSettings, viewModel)
|
|
||||||
}
|
}
|
||||||
|
composable<Route.TunnelAutoTunnel> {
|
||||||
|
val args = it.toRoute<Route.TunnelOptions>()
|
||||||
|
val config = appUiState.tunnels.first { it.id == args.id }
|
||||||
|
TunnelAutoTunnelScreen(config, appUiState.appSettings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BackHandler {
|
BackHandler {
|
||||||
if (navController.currentDestination?.route != Route.Main::class.qualifiedName) {
|
if (navController.previousBackStackEntry == null || !navController.popBackStack()) {
|
||||||
navController.popBackStack()
|
|
||||||
} else {
|
|
||||||
this@MainActivity.finish()
|
this@MainActivity.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,22 +263,5 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
checkPermissionAndNotify()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkPermissionAndNotify() {
|
|
||||||
val hasLocation = ContextCompat.checkSelfPermission(
|
|
||||||
this,
|
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
|
||||||
) == PackageManager.PERMISSION_GRANTED
|
|
||||||
if (lastLocationPermissionState != hasLocation) {
|
|
||||||
Timber.d("Location permission changed to: $hasLocation")
|
|
||||||
if (hasLocation) {
|
|
||||||
networkMonitor.sendLocationPermissionsGrantedBroadcast()
|
|
||||||
}
|
|
||||||
lastLocationPermissionState = hasLocation
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import androidx.work.Configuration
|
|||||||
import com.wireguard.android.backend.GoBackend
|
import com.wireguard.android.backend.GoBackend
|
||||||
import com.zaneschepke.logcatter.LogReader
|
import com.zaneschepke.logcatter.LogReader
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
|
||||||
@@ -19,6 +18,7 @@ import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
|||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
||||||
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -86,14 +86,15 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
|
|||||||
tunnelManager.startTunnel(it)
|
tunnelManager.startTunnel(it)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Timber.w("Always-on VPN is not enabled in app settings")
|
Timber.Forest.w("Always-on VPN is not enabled in app settings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ServiceWorker.start(this)
|
|
||||||
|
|
||||||
applicationScope.launch {
|
applicationScope.launch {
|
||||||
|
withContext(mainDispatcher) {
|
||||||
|
if (appDataRepository.appState.isLocalLogsEnabled() && !isRunningOnTv()) logReader.initialize()
|
||||||
|
}
|
||||||
if (!appDataRepository.settings.get().isKernelEnabled) {
|
if (!appDataRepository.settings.get().isKernelEnabled) {
|
||||||
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
|
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
|
||||||
}
|
}
|
||||||
@@ -102,9 +103,6 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
|
|||||||
LocaleUtil.changeLocale(it)
|
LocaleUtil.changeLocale(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
appDataRepository.appState.isLocalLogsEnabled().let { enabled ->
|
|
||||||
if (enabled) logReader.start()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,19 +132,6 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
|
|||||||
return foreground
|
return foreground
|
||||||
}
|
}
|
||||||
|
|
||||||
@Volatile
|
|
||||||
private var lastActiveTunnels: List<Int> = emptyList()
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun getLastActiveTunnels(): List<Int> {
|
|
||||||
return lastActiveTunnels
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun setLastActiveTunnels(newTunnels: List<Int>) {
|
|
||||||
lastActiveTunnels = newTunnels
|
|
||||||
}
|
|
||||||
|
|
||||||
lateinit var instance: WireGuardAutoTunnel
|
lateinit var instance: WireGuardAutoTunnel
|
||||||
private set
|
private set
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-31
@@ -3,61 +3,42 @@ 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.domain.repository.AppDataRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class RestartReceiver : BroadcastReceiver() {
|
class AppUpdateReceiver : BroadcastReceiver() {
|
||||||
@Inject
|
|
||||||
lateinit var appDataRepository: AppDataRepository
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
@ApplicationScope
|
@ApplicationScope
|
||||||
lateinit var applicationScope: CoroutineScope
|
lateinit var applicationScope: CoroutineScope
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var appDataRepository: AppDataRepository
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var serviceManager: ServiceManager
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var tunnelManager: TunnelManager
|
lateinit var tunnelManager: TunnelManager
|
||||||
|
|
||||||
@Inject
|
|
||||||
@IoDispatcher
|
|
||||||
lateinit var ioDispatcher: CoroutineDispatcher
|
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
val action = intent.action ?: return
|
if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return
|
||||||
if (action != Intent.ACTION_BOOT_COMPLETED &&
|
|
||||||
action != Intent.ACTION_MY_PACKAGE_REPLACED &&
|
|
||||||
action != "com.htc.intent.action.QUICKBOOT_POWERON"
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.d("RestartReceiver triggered with action: ${intent.action}")
|
|
||||||
serviceManager.updateTunnelTile()
|
serviceManager.updateTunnelTile()
|
||||||
serviceManager.updateAutoTunnelTile()
|
serviceManager.updateAutoTunnelTile()
|
||||||
applicationScope.launch(ioDispatcher) {
|
applicationScope.launch {
|
||||||
val settings = appDataRepository.settings.get()
|
with(appDataRepository.settings.get()) {
|
||||||
if (settings.isRestoreOnBootEnabled) {
|
if (isRestoreOnBootEnabled) {
|
||||||
if (settings.isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) {
|
// If auto tunnel is enabled, just start it and let auto tunnel start appropriate tun
|
||||||
Timber.d("Starting auto-tunnel on boot/update")
|
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@launch serviceManager.startAutoTunnel(true)
|
||||||
serviceManager.startAutoTunnel()
|
|
||||||
} else {
|
|
||||||
Timber.d("Restoring previous tunnel state")
|
|
||||||
tunnelManager.restorePreviousState()
|
tunnelManager.restorePreviousState()
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Timber.d("Restore on boot disabled, skipping")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.core.broadcast
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class BootReceiver : BroadcastReceiver() {
|
||||||
|
@Inject
|
||||||
|
lateinit var appDataRepository: AppDataRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@ApplicationScope
|
||||||
|
lateinit var applicationScope: CoroutineScope
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var tunnelManager: TunnelManager
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
if (Intent.ACTION_BOOT_COMPLETED != intent.action) return
|
||||||
|
serviceManager.updateTunnelTile()
|
||||||
|
serviceManager.updateAutoTunnelTile()
|
||||||
|
applicationScope.launch {
|
||||||
|
with(appDataRepository.settings.get()) {
|
||||||
|
if (isRestoreOnBootEnabled) {
|
||||||
|
// If auto tunnel is enabled, just start it and let auto tunnel start appropriate tun
|
||||||
|
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@launch serviceManager.startAutoTunnel(true)
|
||||||
|
tunnelManager.restorePreviousState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-4
@@ -3,10 +3,10 @@ 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.domain.repository.TunnelRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -32,8 +32,8 @@ class KernelReceiver : BroadcastReceiver() {
|
|||||||
val action = intent.action ?: return
|
val action = intent.action ?: return
|
||||||
applicationScope.launch {
|
applicationScope.launch {
|
||||||
if (action == REFRESH_TUNNELS_ACTION) {
|
if (action == REFRESH_TUNNELS_ACTION) {
|
||||||
tunnelManager.runningTunnelNames().forEach { name ->
|
tunnelManager.runningTunnelNames().forEach {
|
||||||
val tunnel = tunnelRepository.findByTunnelName(name)
|
val tunnel = tunnelRepository.findByTunnelName(it)
|
||||||
tunnel?.let {
|
tunnel?.let {
|
||||||
tunnelRepository.save(it.copy(isActive = true))
|
tunnelRepository.save(it.copy(isActive = true))
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-6
@@ -4,9 +4,9 @@ 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.NotificationManager
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
@@ -36,15 +36,11 @@ class NotificationActionReceiver : BroadcastReceiver() {
|
|||||||
NotificationAction.AUTO_TUNNEL_OFF.name -> serviceManager.stopAutoTunnel()
|
NotificationAction.AUTO_TUNNEL_OFF.name -> serviceManager.stopAutoTunnel()
|
||||||
NotificationAction.TUNNEL_OFF.name -> {
|
NotificationAction.TUNNEL_OFF.name -> {
|
||||||
val tunnelId = intent.getIntExtra(NotificationManager.EXTRA_ID, 0)
|
val tunnelId = intent.getIntExtra(NotificationManager.EXTRA_ID, 0)
|
||||||
if (tunnelId == STOP_ALL_TUNNELS_ID) return@launch tunnelManager.stopTunnel()
|
if (tunnelId == 0) return@launch tunnelManager.stopTunnel()
|
||||||
val tunnel = tunnelRepository.getById(tunnelId)
|
val tunnel = tunnelRepository.getById(tunnelId)
|
||||||
tunnelManager.stopTunnel(tunnel)
|
tunnelManager.stopTunnel(tunnel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val STOP_ALL_TUNNELS_ID = 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+161
@@ -0,0 +1,161 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.core.network
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.Network
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.net.NetworkRequest
|
||||||
|
import android.net.wifi.SupplicantState
|
||||||
|
import android.net.wifi.WifiManager
|
||||||
|
import android.os.Build
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.state.ConnectivityState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class InternetConnectivityMonitor
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||||
|
) : NetworkMonitor {
|
||||||
|
|
||||||
|
private val connectivityManager =
|
||||||
|
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
|
||||||
|
@get:Synchronized @set:Synchronized
|
||||||
|
private var wifiCapabilities: NetworkCapabilities? = null
|
||||||
|
|
||||||
|
@get:Synchronized @set:Synchronized
|
||||||
|
private var wifiNetworkChanged: Boolean = false
|
||||||
|
|
||||||
|
override val didWifiChangeSinceLastCapabilitiesQuery: Boolean
|
||||||
|
get() = wifiNetworkChanged
|
||||||
|
|
||||||
|
override val status = callbackFlow {
|
||||||
|
|
||||||
|
var wifiState: Boolean = false
|
||||||
|
var ethernetState: Boolean = false
|
||||||
|
var cellularState: Boolean = false
|
||||||
|
|
||||||
|
fun emitState() {
|
||||||
|
trySend(ConnectivityState(wifiState, ethernetState, cellularState))
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentNetwork = connectivityManager.activeNetwork
|
||||||
|
if (currentNetwork == null) {
|
||||||
|
emitState()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateCapabilityState(up: Boolean, network: Network) {
|
||||||
|
with(connectivityManager.getNetworkCapabilities(network)) {
|
||||||
|
when {
|
||||||
|
this == null -> return
|
||||||
|
hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> wifiState = up
|
||||||
|
hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ->
|
||||||
|
cellularState = up
|
||||||
|
|
||||||
|
hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) ->
|
||||||
|
ethernetState = up
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onWifiChange(network: Network, callback: () -> Unit) {
|
||||||
|
if (connectivityManager.getNetworkCapabilities(network)?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true) {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAvailable(network: Network) {
|
||||||
|
onWifiChange(network) {
|
||||||
|
wifiNetworkChanged = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||||
|
onWifiChange(network) {
|
||||||
|
wifiCapabilities = networkCapabilities
|
||||||
|
}
|
||||||
|
updateCapabilityState(true, network)
|
||||||
|
emitState()
|
||||||
|
}
|
||||||
|
|
||||||
|
val networkStatusCallback =
|
||||||
|
when (Build.VERSION.SDK_INT) {
|
||||||
|
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
|
||||||
|
object :
|
||||||
|
ConnectivityManager.NetworkCallback(
|
||||||
|
FLAG_INCLUDE_LOCATION_INFO,
|
||||||
|
) {
|
||||||
|
override fun onAvailable(network: Network) {
|
||||||
|
onAvailable(network)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLost(network: Network) {
|
||||||
|
updateCapabilityState(false, network)
|
||||||
|
emitState()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||||
|
onCapabilitiesChanged(network, networkCapabilities)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
object : ConnectivityManager.NetworkCallback() {
|
||||||
|
override fun onAvailable(network: Network) {
|
||||||
|
onAvailable(network)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLost(network: Network) {
|
||||||
|
updateCapabilityState(false, network)
|
||||||
|
emitState()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||||
|
onCapabilitiesChanged(network, networkCapabilities)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val request =
|
||||||
|
NetworkRequest.Builder()
|
||||||
|
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||||
|
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
|
||||||
|
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||||
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||||
|
.build()
|
||||||
|
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
|
||||||
|
|
||||||
|
awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) }
|
||||||
|
}.flowOn(ioDispatcher)
|
||||||
|
|
||||||
|
override fun getWifiCapabilities(): NetworkCapabilities? {
|
||||||
|
wifiNetworkChanged = false
|
||||||
|
return wifiCapabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getNetworkName(networkCapabilities: NetworkCapabilities, context: Context): String? {
|
||||||
|
var ssid = networkCapabilities.getWifiName()
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
|
||||||
|
val wifiManager =
|
||||||
|
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val info = wifiManager.connectionInfo
|
||||||
|
if (info.supplicantState === SupplicantState.COMPLETED) {
|
||||||
|
ssid = info.ssid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ssid?.trim('"')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.core.network
|
||||||
|
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.net.wifi.WifiInfo
|
||||||
|
import android.os.Build
|
||||||
|
|
||||||
|
fun NetworkCapabilities.getWifiName(): String? {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
val info: WifiInfo
|
||||||
|
if (transportInfo is WifiInfo) {
|
||||||
|
info = transportInfo as WifiInfo
|
||||||
|
return info.ssid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.core.network
|
||||||
|
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.state.ConnectivityState
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface NetworkMonitor {
|
||||||
|
val status: Flow<ConnectivityState>
|
||||||
|
|
||||||
|
// util to help limit location queries
|
||||||
|
val didWifiChangeSinceLastCapabilitiesQuery: Boolean
|
||||||
|
fun getWifiCapabilities(): NetworkCapabilities?
|
||||||
|
}
|
||||||
+2
-1
@@ -4,8 +4,8 @@ import android.app.Notification
|
|||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification.NotificationChannels
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification.NotificationChannels
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
|
|
||||||
interface NotificationManager {
|
interface NotificationManager {
|
||||||
@@ -39,6 +39,7 @@ interface NotificationManager {
|
|||||||
fun show(notificationId: Int, notification: Notification)
|
fun show(notificationId: Int, notification: Notification)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
const val KERNEL_SERVICE_NOTIFICATION_ID = 123
|
||||||
const val AUTO_TUNNEL_NOTIFICATION_ID = 122
|
const val AUTO_TUNNEL_NOTIFICATION_ID = 122
|
||||||
const val VPN_NOTIFICATION_ID = 100
|
const val VPN_NOTIFICATION_ID = 100
|
||||||
const val EXTRA_ID = "id"
|
const val EXTRA_ID = "id"
|
||||||
|
|||||||
+1
-1
@@ -12,9 +12,9 @@ import android.graphics.Color
|
|||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
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.MainActivity
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager.Companion.EXTRA_ID
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager.Companion.EXTRA_ID
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
|
|||||||
+50
-61
@@ -3,43 +3,35 @@ package com.zaneschepke.wireguardautotunnel.core.service
|
|||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.VpnService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelControlTile
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelControlTile
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
|
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
|
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
|
||||||
import jakarta.inject.Inject
|
import jakarta.inject.Inject
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class ServiceManager @Inject constructor(
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
private val context: Context,
|
class ServiceManager
|
||||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
@Inject constructor(private val context: Context, private val ioDispatcher: CoroutineDispatcher, private val appDataRepository: AppDataRepository) {
|
||||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
|
||||||
@MainDispatcher private val mainDispatcher: CoroutineDispatcher,
|
|
||||||
private val appDataRepository: AppDataRepository,
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val autoTunnelMutex = Mutex()
|
|
||||||
|
|
||||||
private val _autoTunnelActive = MutableStateFlow(false)
|
private val _autoTunnelActive = MutableStateFlow(false)
|
||||||
|
|
||||||
val autoTunnelActive = _autoTunnelActive.asStateFlow()
|
val autoTunnelActive = _autoTunnelActive.asStateFlow()
|
||||||
|
|
||||||
var autoTunnelService = CompletableDeferred<AutoTunnelService>()
|
var autoTunnelService = CompletableDeferred<AutoTunnelService>()
|
||||||
var backgroundService = CompletableDeferred<TunnelForegroundService>()
|
var backgroundService = CompletableDeferred<TunnelForegroundService>()
|
||||||
|
var autoTunnelTile = CompletableDeferred<AutoTunnelControlTile>()
|
||||||
|
var tunnelControlTile = CompletableDeferred<TunnelControlTile>()
|
||||||
|
|
||||||
private fun <T : Service> startService(cls: Class<T>, background: Boolean) {
|
private fun <T : Service> startService(cls: Class<T>, background: Boolean) {
|
||||||
runCatching {
|
runCatching {
|
||||||
@@ -52,79 +44,76 @@ class ServiceManager @Inject constructor(
|
|||||||
}.onFailure { Timber.e(it) }
|
}.onFailure { Timber.e(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasVpnPermission(): Boolean {
|
suspend fun startAutoTunnel(background: Boolean) {
|
||||||
return VpnService.prepare(context) == null
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun startAutoTunnel() {
|
|
||||||
autoTunnelMutex.withLock {
|
|
||||||
val settings = appDataRepository.settings.get()
|
val settings = appDataRepository.settings.get()
|
||||||
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true))
|
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true))
|
||||||
if (autoTunnelService.isCompleted) {
|
if (autoTunnelService.isCompleted) return _autoTunnelActive.update { true }
|
||||||
_autoTunnelActive.update { true }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
runCatching {
|
runCatching {
|
||||||
autoTunnelService = CompletableDeferred()
|
startService(AutoTunnelService::class.java, background)
|
||||||
startService(AutoTunnelService::class.java, !WireGuardAutoTunnel.isForeground())
|
autoTunnelService.await()
|
||||||
|
autoTunnelService.getCompleted().start()
|
||||||
_autoTunnelActive.update { true }
|
_autoTunnelActive.update { true }
|
||||||
}.onFailure {
|
updateAutoTunnelTile()
|
||||||
Timber.e(it)
|
|
||||||
_autoTunnelActive.update { false }
|
|
||||||
}
|
|
||||||
withContext(mainDispatcher) { updateAutoTunnelTile() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun stopAutoTunnel() {
|
|
||||||
autoTunnelMutex.withLock {
|
|
||||||
val settings = appDataRepository.settings.get()
|
|
||||||
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = false))
|
|
||||||
if (!autoTunnelService.isCompleted) return
|
|
||||||
runCatching {
|
|
||||||
val service = autoTunnelService.await()
|
|
||||||
service.stop()
|
|
||||||
_autoTunnelActive.update { false }
|
|
||||||
autoTunnelService = CompletableDeferred()
|
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Timber.e(it)
|
Timber.e(it)
|
||||||
}
|
}
|
||||||
withContext(mainDispatcher) { updateAutoTunnelTile() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startTunnelForegroundService() {
|
suspend fun startBackgroundService(tunnelConf: TunnelConf) {
|
||||||
if (backgroundService.isCompleted) return
|
if (backgroundService.isCompleted) return
|
||||||
runCatching {
|
runCatching {
|
||||||
backgroundService = CompletableDeferred()
|
startService(TunnelForegroundService::class.java, true)
|
||||||
startService(TunnelForegroundService::class.java, !WireGuardAutoTunnel.isForeground())
|
backgroundService.await()
|
||||||
|
backgroundService.getCompleted().start(tunnelConf)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Timber.e(it)
|
Timber.e(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun stopTunnelForegroundService() {
|
fun stopBackgroundService() {
|
||||||
if (!backgroundService.isCompleted) return
|
if (!backgroundService.isCompleted) return
|
||||||
runCatching {
|
runCatching {
|
||||||
val service = backgroundService.await()
|
backgroundService.getCompleted().stop()
|
||||||
service.stop()
|
|
||||||
backgroundService = CompletableDeferred()
|
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Timber.e(it)
|
Timber.e(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleAutoTunnel() {
|
suspend fun toggleAutoTunnel(background: Boolean) {
|
||||||
applicationScope.launch(ioDispatcher) {
|
withContext(ioDispatcher) {
|
||||||
if (_autoTunnelActive.value) stopAutoTunnel() else startAutoTunnel()
|
if (_autoTunnelActive.value) return@withContext stopAutoTunnel()
|
||||||
|
startAutoTunnel(background)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateAutoTunnelTile() {
|
fun updateAutoTunnelTile() {
|
||||||
|
if (autoTunnelTile.isCompleted) {
|
||||||
|
autoTunnelTile.getCompleted().updateTileState()
|
||||||
|
} else {
|
||||||
context.requestAutoTunnelTileServiceUpdate()
|
context.requestAutoTunnelTileServiceUpdate()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun updateTunnelTile() {
|
fun updateTunnelTile() {
|
||||||
|
if (tunnelControlTile.isCompleted) {
|
||||||
|
tunnelControlTile.getCompleted().updateTileState()
|
||||||
|
} else {
|
||||||
context.requestTunnelTileServiceStateUpdate()
|
context.requestTunnelTileServiceStateUpdate()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun stopAutoTunnel() {
|
||||||
|
withContext(ioDispatcher) {
|
||||||
|
val settings = appDataRepository.settings.get()
|
||||||
|
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = false))
|
||||||
|
if (!autoTunnelService.isCompleted) return@withContext
|
||||||
|
runCatching {
|
||||||
|
autoTunnelService.getCompleted().stop()
|
||||||
|
_autoTunnelActive.update { false }
|
||||||
|
updateAutoTunnelTile()
|
||||||
|
}.onFailure {
|
||||||
|
Timber.e(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-210
@@ -5,38 +5,14 @@ import android.content.Intent
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.core.app.ServiceCompat
|
import androidx.core.app.ServiceCompat
|
||||||
import androidx.lifecycle.LifecycleService
|
import androidx.lifecycle.LifecycleService
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
|
||||||
import com.zaneschepke.networkmonitor.NetworkStatus
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.distinctByKeys
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.NonCancellable
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -48,32 +24,9 @@ class TunnelForegroundService : LifecycleService() {
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var serviceManager: ServiceManager
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var networkMonitor: NetworkMonitor
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
@IoDispatcher
|
|
||||||
lateinit var ioDispatcher: CoroutineDispatcher
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var tunnelRepo: TunnelRepository
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var tunnelManager: TunnelManager
|
|
||||||
|
|
||||||
private val isNetworkConnected = MutableStateFlow(true)
|
|
||||||
|
|
||||||
private val tunnelJobs = ConcurrentHashMap<TunnelConf, Job>()
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
serviceManager.backgroundService.complete(this)
|
serviceManager.backgroundService.complete(this)
|
||||||
ServiceCompat.startForeground(
|
|
||||||
this@TunnelForegroundService,
|
|
||||||
NotificationManager.VPN_NOTIFICATION_ID,
|
|
||||||
onCreateNotification(),
|
|
||||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder? {
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
@@ -84,156 +37,29 @@ class TunnelForegroundService : LifecycleService() {
|
|||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
super.onStartCommand(intent, flags, startId)
|
super.onStartCommand(intent, flags, startId)
|
||||||
serviceManager.backgroundService.complete(this)
|
serviceManager.backgroundService.complete(this)
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
fun start(tunnelConf: TunnelConf) {
|
||||||
ServiceCompat.startForeground(
|
ServiceCompat.startForeground(
|
||||||
this@TunnelForegroundService,
|
this@TunnelForegroundService,
|
||||||
NotificationManager.VPN_NOTIFICATION_ID,
|
NotificationManager.KERNEL_SERVICE_NOTIFICATION_ID,
|
||||||
onCreateNotification(),
|
createNotification(tunnelConf),
|
||||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
||||||
)
|
)
|
||||||
start()
|
|
||||||
return START_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
fun start() = lifecycleScope.launch {
|
|
||||||
tunnelManager.activeTunnels.distinctByKeys().collect { tuns ->
|
|
||||||
if (tuns.isEmpty() && tunnelJobs.isEmpty()) return@collect
|
|
||||||
if (tuns.isEmpty() && tunnelJobs.isNotEmpty()) {
|
|
||||||
return@collect tunnelJobs.forEach { (key, _) ->
|
|
||||||
Timber.d("Stopping all tunnel jobs")
|
|
||||||
tunnelJobs[key]?.cancel()
|
|
||||||
tunnelJobs.remove(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val (jobsToStop, jobsToStart) = findMissingKeys(tuns, tunnelJobs)
|
|
||||||
if (jobsToStop.isEmpty() && jobsToStart.isEmpty()) return@collect
|
|
||||||
jobsToStop.forEach { tun ->
|
|
||||||
Timber.d("Stopping tunnel jobs for ${tun.tunName}")
|
|
||||||
tunnelJobs[tun]?.cancel()
|
|
||||||
tunnelJobs.remove(tun)
|
|
||||||
}
|
|
||||||
jobsToStart.forEach { tun ->
|
|
||||||
Timber.d("Starting tunnel jobs for ${tun.tunName}")
|
|
||||||
tunnelJobs += (tun to startTunnelJobs(tun))
|
|
||||||
}
|
|
||||||
updateServiceNotification()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Would be cool to have this include kill switch
|
|
||||||
// TODO also we need to include errors
|
|
||||||
private fun updateServiceNotification() {
|
|
||||||
val notification = when (tunnelJobs.size) {
|
|
||||||
0 -> onCreateNotification()
|
|
||||||
1 -> createTunnelNotification(tunnelJobs.keys.first())
|
|
||||||
else -> createTunnelsNotification()
|
|
||||||
}
|
|
||||||
ServiceCompat.startForeground(
|
|
||||||
this@TunnelForegroundService,
|
|
||||||
NotificationManager.VPN_NOTIFICATION_ID,
|
|
||||||
notification,
|
|
||||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// use same scope so we can cancel all of these
|
|
||||||
private fun startTunnelJobs(tunnelConf: TunnelConf) = lifecycleScope.launch {
|
|
||||||
// monitor if we have internet connectivity
|
|
||||||
launch { startNetworkMonitorJob() }
|
|
||||||
// job to trigger stats emit on interval
|
|
||||||
launch { startTunnelStatsJob(tunnelConf) }
|
|
||||||
// monitor changes to the tunnel config
|
|
||||||
launch { startTunnelConfChangesJob(tunnelConf) }
|
|
||||||
// monitor tunnel ping
|
|
||||||
launch { startPingJob(tunnelConf) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findMissingKeys(map1: Map<TunnelConf, Any>, map2: Map<TunnelConf, Any>): Pair<Set<TunnelConf>, Set<TunnelConf>> {
|
|
||||||
val missingMap1 = map2.keys - map1.keys
|
|
||||||
val missingMap2 = map1.keys - map2.keys
|
|
||||||
return missingMap1 to missingMap2
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun startTunnelConfChangesJob(tunnelConf: TunnelConf) {
|
|
||||||
tunnelRepo.flow
|
|
||||||
.flowOn(ioDispatcher)
|
|
||||||
.map { storedTunnels ->
|
|
||||||
storedTunnels.firstOrNull { it.id == tunnelConf.id }
|
|
||||||
}
|
|
||||||
.filterNotNull()
|
|
||||||
// only emit when one of these 3 values change
|
|
||||||
.distinctUntilChanged { old, new ->
|
|
||||||
old == new
|
|
||||||
}
|
|
||||||
.collect { storedTunnel ->
|
|
||||||
if (tunnelConf != storedTunnel) {
|
|
||||||
Timber.d("Config changed for ${storedTunnel.tunName}, bouncing")
|
|
||||||
// let this complete, even after cancel
|
|
||||||
withContext(NonCancellable) {
|
|
||||||
tunnelManager.bounceTunnel(storedTunnel, TunnelStatus.StopReason.CONFIG_CHANGED)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun startNetworkMonitorJob() {
|
|
||||||
networkMonitor.networkStatusFlow
|
|
||||||
.flowOn(ioDispatcher)
|
|
||||||
.collectLatest { status ->
|
|
||||||
val isAvailable = status !is NetworkStatus.Disconnected
|
|
||||||
isNetworkConnected.value = isAvailable
|
|
||||||
Timber.d("Network available: $status")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun startTunnelStatsJob(tunnel: TunnelConf) = coroutineScope {
|
|
||||||
while (isActive) {
|
|
||||||
tunnelManager.updateTunnelStatistics(tunnel)
|
|
||||||
delay(STATS_DELAY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO fix cooldown
|
|
||||||
private suspend fun startPingJob(tunnel: TunnelConf) = coroutineScope {
|
|
||||||
delay(PING_START_DELAY)
|
|
||||||
while (isActive) {
|
|
||||||
val shouldBounce = shouldBounceTunnel(tunnel)
|
|
||||||
val delayMs = if (shouldBounce) {
|
|
||||||
// let this complete, even after cancel
|
|
||||||
withContext(NonCancellable) {
|
|
||||||
tunnelManager.bounceTunnel(tunnel, TunnelStatus.StopReason.PING)
|
|
||||||
}
|
|
||||||
tunnel.pingCooldown ?: Constants.PING_COOLDOWN
|
|
||||||
} else {
|
|
||||||
tunnel.pingInterval ?: Constants.PING_INTERVAL
|
|
||||||
}
|
|
||||||
delay(delayMs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun shouldBounceTunnel(tunnel: TunnelConf): Boolean {
|
|
||||||
if (!isNetworkConnected.value) {
|
|
||||||
Timber.d("Network disconnected, skipping ping for ${tunnel.tunName}")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return runCatching {
|
|
||||||
!tunnel.isTunnelPingable(ioDispatcher)
|
|
||||||
}.onFailure { e ->
|
|
||||||
Timber.e(e, "Ping check failed for ${tunnel.tunName}")
|
|
||||||
}.getOrDefault(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
serviceManager.backgroundService = CompletableDeferred()
|
serviceManager.backgroundService = CompletableDeferred()
|
||||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createTunnelNotification(tunnelConf: TunnelConf): Notification {
|
private fun createNotification(tunnelConf: TunnelConf): Notification {
|
||||||
return notificationManager.createNotification(
|
return notificationManager.createNotification(
|
||||||
WireGuardNotification.NotificationChannels.VPN,
|
WireGuardNotification.NotificationChannels.VPN,
|
||||||
title = "${getString(R.string.tunnel_running)} - ${tunnelConf.tunName}",
|
title = "${getString(R.string.tunnel_running)} - ${tunnelConf.tunName}",
|
||||||
@@ -242,31 +68,4 @@ class TunnelForegroundService : LifecycleService() {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createTunnelsNotification(): Notification {
|
|
||||||
return notificationManager.createNotification(
|
|
||||||
WireGuardNotification.NotificationChannels.VPN,
|
|
||||||
title = "${getString(R.string.tunnel_running)} - ${getString(R.string.multiple)}",
|
|
||||||
actions = listOf(
|
|
||||||
notificationManager.createNotificationAction(NotificationAction.TUNNEL_OFF, 0),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onCreateNotification(): Notification {
|
|
||||||
return notificationManager.createNotification(
|
|
||||||
WireGuardNotification.NotificationChannels.VPN,
|
|
||||||
title = getString(R.string.tunnel_starting),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO add notification handling and optional log reading for restart on handshake failures
|
|
||||||
companion object {
|
|
||||||
const val STATS_DELAY = 1_000L
|
|
||||||
const val PING_START_DELAY = 30_000L
|
|
||||||
// ipv6 disabled or block on network
|
|
||||||
// const val userspaceStartFailed = "Failed to send handshake initiation: write udp [::]"
|
|
||||||
// const val ipv6Fails = "Failed to send data packets: write udp [::]"
|
|
||||||
// const val ipv4Fails = "Failed to send data packets: write udp 0.0.0.0:51820"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+61
-63
@@ -1,43 +1,44 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import androidx.core.app.ServiceCompat
|
import androidx.core.app.ServiceCompat
|
||||||
import androidx.lifecycle.LifecycleService
|
import androidx.lifecycle.LifecycleService
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
import com.wireguard.android.util.RootShell
|
||||||
import com.zaneschepke.networkmonitor.NetworkStatus
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.di.AppShell
|
||||||
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
|
import com.zaneschepke.wireguardautotunnel.di.MainImmediateDispatcher
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
|
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
|
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.network.InternetConnectivityMonitor
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.state.ConnectivityState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
|
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -48,6 +49,10 @@ import javax.inject.Provider
|
|||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class AutoTunnelService : LifecycleService() {
|
class AutoTunnelService : LifecycleService() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@AppShell
|
||||||
|
lateinit var rootShell: Provider<RootShell>
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var networkMonitor: NetworkMonitor
|
lateinit var networkMonitor: NetworkMonitor
|
||||||
|
|
||||||
@@ -64,6 +69,10 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var serviceManager: ServiceManager
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@MainImmediateDispatcher
|
||||||
|
lateinit var mainImmediateDispatcher: CoroutineDispatcher
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var tunnelManager: TunnelManager
|
lateinit var tunnelManager: TunnelManager
|
||||||
|
|
||||||
@@ -73,12 +82,16 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
|
|
||||||
private var wakeLock: PowerManager.WakeLock? = null
|
private var wakeLock: PowerManager.WakeLock? = null
|
||||||
|
|
||||||
private var killSwitchJob: Job? = null
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
serviceManager.autoTunnelService.complete(this)
|
serviceManager.autoTunnelService.complete(this)
|
||||||
|
lifecycleScope.launch(mainImmediateDispatcher) {
|
||||||
|
runCatching {
|
||||||
launchWatcherNotification()
|
launchWatcherNotification()
|
||||||
|
}.onFailure {
|
||||||
|
Timber.e(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder? {
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
@@ -90,17 +103,18 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
super.onStartCommand(intent, flags, startId)
|
super.onStartCommand(intent, flags, startId)
|
||||||
Timber.d("onStartCommand executed with startId: $startId")
|
Timber.d("onStartCommand executed with startId: $startId")
|
||||||
serviceManager.autoTunnelService.complete(this)
|
serviceManager.autoTunnelService.complete(this)
|
||||||
start()
|
return START_NOT_STICKY
|
||||||
return START_STICKY
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
|
lifecycleScope.launch(mainImmediateDispatcher) {
|
||||||
launchWatcherNotification()
|
launchWatcherNotification()
|
||||||
initWakeLock()
|
initWakeLock()
|
||||||
|
}
|
||||||
startAutoTunnelJob()
|
startAutoTunnelJob()
|
||||||
startAutoTunnelStateJob()
|
startAutoTunnelStateJob()
|
||||||
killSwitchJob = startKillSwitchJob()
|
startKillSwitchJob()
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Timber.e(it)
|
Timber.e(it)
|
||||||
}
|
}
|
||||||
@@ -113,20 +127,9 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
serviceManager.autoTunnelService = CompletableDeferred()
|
serviceManager.autoTunnelService = CompletableDeferred()
|
||||||
restoreVpnKillSwitch()
|
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreVpnKillSwitch() {
|
|
||||||
with(autoTunnelStateFlow.value) {
|
|
||||||
if (settings.isVpnKillSwitchEnabled && tunnelManager.getBackendState() != BackendState.KILL_SWITCH_ACTIVE) {
|
|
||||||
killSwitchJob?.cancel()
|
|
||||||
val allowedIps = if (settings.isLanOnKillSwitchEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS else emptyList()
|
|
||||||
tunnelManager.setBackendState(BackendState.KILL_SWITCH_ACTIVE, allowedIps)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchWatcherNotification(description: String = getString(R.string.monitoring_state_changes)) {
|
private fun launchWatcherNotification(description: String = getString(R.string.monitoring_state_changes)) {
|
||||||
val notification =
|
val notification =
|
||||||
notificationManager.createNotification(
|
notificationManager.createNotification(
|
||||||
@@ -159,54 +162,49 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildNetworkState(networkStatus: NetworkStatus): NetworkState {
|
private suspend fun buildNetworkState(connectivityState: ConnectivityState): NetworkState {
|
||||||
return with(autoTunnelStateFlow.value.networkState) {
|
return with(autoTunnelStateFlow.value.networkState) {
|
||||||
val wifiName = when (networkStatus) {
|
val wifiName = when {
|
||||||
is NetworkStatus.Connected -> {
|
connectivityState.wifiAvailable &&
|
||||||
networkStatus.wifiSsid
|
(wifiName == null || wifiName == Constants.UNREADABLE_SSID || networkMonitor.didWifiChangeSinceLastCapabilitiesQuery) -> {
|
||||||
|
networkMonitor.getWifiCapabilities()?.let { getWifiName(it) } ?: wifiName
|
||||||
}
|
}
|
||||||
else -> null
|
!connectivityState.wifiAvailable -> null
|
||||||
|
else -> wifiName
|
||||||
}
|
}
|
||||||
copy(
|
copy(
|
||||||
isWifiConnected = networkStatus.wifiConnected,
|
isWifiConnected = connectivityState.wifiAvailable,
|
||||||
isMobileDataConnected = networkStatus.cellularConnected,
|
isMobileDataConnected = connectivityState.cellularAvailable,
|
||||||
isEthernetConnected = networkStatus.ethernetConnected,
|
isEthernetConnected = isEthernetConnected,
|
||||||
wifiName = wifiName,
|
wifiName = wifiName,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
private fun startAutoTunnelStateJob() = lifecycleScope.launch(ioDispatcher) {
|
private fun startAutoTunnelStateJob() = lifecycleScope.launch(ioDispatcher) {
|
||||||
combine(
|
combine(
|
||||||
combineSettings(),
|
combineSettings(),
|
||||||
appDataRepository.get().settings.flow
|
networkMonitor.status.map {
|
||||||
.distinctUntilChanged { old, new -> old.isKernelEnabled == new.isKernelEnabled } // Only emit when isKernelEnabled changes
|
buildNetworkState(it)
|
||||||
.flatMapLatest {
|
}.distinctUntilChanged(),
|
||||||
networkMonitor.networkStatusFlow
|
|
||||||
.flowOn(ioDispatcher)
|
|
||||||
.map { buildNetworkState(it) }
|
|
||||||
}
|
|
||||||
.distinctUntilChanged(),
|
|
||||||
) { double, networkState ->
|
) { double, networkState ->
|
||||||
AutoTunnelState(
|
AutoTunnelState(tunnelManager.activeTunnels().value, networkState, double.first, double.second)
|
||||||
tunnelManager.activeTunnels.value,
|
|
||||||
networkState,
|
|
||||||
double.first,
|
|
||||||
double.second,
|
|
||||||
)
|
|
||||||
}.collect { state ->
|
}.collect { state ->
|
||||||
autoTunnelStateFlow.update {
|
autoTunnelStateFlow.update {
|
||||||
it.copy(
|
it.copy(activeTunnels = state.activeTunnels, networkState = state.networkState, settings = state.settings, tunnels = state.tunnels)
|
||||||
activeTunnels = state.activeTunnels,
|
|
||||||
networkState = state.networkState,
|
|
||||||
settings = state.settings,
|
|
||||||
tunnels = state.tunnels,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun getWifiName(wifiCapabilities: NetworkCapabilities): String? {
|
||||||
|
val setting = appDataRepository.get().settings.get()
|
||||||
|
return if (setting.isWifiNameByShellEnabled) {
|
||||||
|
rootShell.get().getCurrentWifiName()
|
||||||
|
} else {
|
||||||
|
InternetConnectivityMonitor.getNetworkName(wifiCapabilities, this@AutoTunnelService)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun combineSettings(): Flow<Pair<AppSettings, Tunnels>> {
|
private fun combineSettings(): Flow<Pair<AppSettings, Tunnels>> {
|
||||||
return combine(
|
return combine(
|
||||||
appDataRepository.get().settings.flow,
|
appDataRepository.get().settings.flow,
|
||||||
@@ -243,7 +241,7 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
Timber.d("Starting with debounce delay of: ${settings.debounceDelaySeconds} seconds")
|
Timber.d("Starting with debounce delay of: ${settings.debounceDelaySeconds} seconds")
|
||||||
autoTunnelStateFlow.debounce(settings.debounceDelayMillis()).collect { watcherState ->
|
autoTunnelStateFlow.debounce(settings.debounceDelayMillis()).collect { watcherState ->
|
||||||
if (watcherState == defaultState) return@collect
|
if (watcherState == defaultState) return@collect
|
||||||
Timber.d("New auto tunnel state emitted ${watcherState.networkState}")
|
Timber.d("New auto tunnel state emitted")
|
||||||
when (val event = watcherState.asAutoTunnelEvent()) {
|
when (val event = watcherState.asAutoTunnelEvent()) {
|
||||||
is AutoTunnelEvent.Start -> (event.tunnelConf ?: appDataRepository.get().getPrimaryOrFirstTunnel())?.let {
|
is AutoTunnelEvent.Start -> (event.tunnelConf ?: appDataRepository.get().getPrimaryOrFirstTunnel())?.let {
|
||||||
tunnelManager.startTunnel(it)
|
tunnelManager.startTunnel(it)
|
||||||
|
|||||||
+20
-26
@@ -4,65 +4,62 @@ import android.content.Intent
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.service.quicksettings.Tile
|
import android.service.quicksettings.Tile
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.lifecycle.LifecycleRegistry
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class AutoTunnelControlTile : TileService(), LifecycleOwner {
|
class AutoTunnelControlTile : TileService() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var appDataRepository: AppDataRepository
|
lateinit var appDataRepository: AppDataRepository
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var serviceManager: ServiceManager
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
|
@Inject
|
||||||
|
@ApplicationScope
|
||||||
|
lateinit var applicationScope: CoroutineScope
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
serviceManager.autoTunnelTile.complete(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
serviceManager.autoTunnelTile = CompletableDeferred()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartListening() {
|
override fun onStartListening() {
|
||||||
super.onStartListening()
|
super.onStartListening()
|
||||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
|
serviceManager.autoTunnelTile.complete(this)
|
||||||
Timber.d("Start listening called for auto tunnel tile")
|
applicationScope.launch {
|
||||||
lifecycleScope.launch {
|
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
|
||||||
serviceManager.autoTunnelActive.collect {
|
updateTileState()
|
||||||
if (it) return@collect setActive()
|
|
||||||
setInactive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lifecycleScope.launch {
|
|
||||||
appDataRepository.tunnels.flow.collect {
|
|
||||||
if (it.isEmpty()) {
|
|
||||||
setUnavailable()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateTileState() {
|
||||||
|
serviceManager.autoTunnelActive.value.let {
|
||||||
|
if (it) setActive() else setInactive()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
super.onClick()
|
super.onClick()
|
||||||
unlockAndRun {
|
unlockAndRun {
|
||||||
lifecycleScope.launch {
|
applicationScope.launch {
|
||||||
if (serviceManager.autoTunnelActive.value) {
|
if (serviceManager.autoTunnelActive.value) {
|
||||||
serviceManager.stopAutoTunnel()
|
serviceManager.stopAutoTunnel()
|
||||||
setInactive()
|
setInactive()
|
||||||
} else {
|
} else {
|
||||||
serviceManager.startAutoTunnel()
|
serviceManager.startAutoTunnel(true)
|
||||||
setActive()
|
setActive()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,7 +97,4 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
|
|||||||
qsTile.updateTile()
|
qsTile.updateTile()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override val lifecycle: Lifecycle
|
|
||||||
get() = lifecycleRegistry
|
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-82
@@ -5,129 +5,70 @@ import android.os.Build
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.service.quicksettings.Tile
|
import android.service.quicksettings.Tile
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.lifecycle.LifecycleRegistry
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class TunnelControlTile : TileService(), LifecycleOwner {
|
class TunnelControlTile : TileService() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var appDataRepository: AppDataRepository
|
lateinit var appDataRepository: AppDataRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@ApplicationScope
|
||||||
|
lateinit var applicationScope: CoroutineScope
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var serviceManager: ServiceManager
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var tunnelManager: TunnelManager
|
lateinit var tunnelManager: TunnelManager
|
||||||
|
|
||||||
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
|
|
||||||
|
|
||||||
private var isCollecting = false
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
serviceManager.tunnelControlTile.complete(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
serviceManager.tunnelControlTile = CompletableDeferred()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartListening() {
|
override fun onStartListening() {
|
||||||
super.onStartListening()
|
super.onStartListening()
|
||||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
|
Timber.d("Start listening called")
|
||||||
Timber.d("Start listening called for tunnel tile")
|
serviceManager.tunnelControlTile.complete(this)
|
||||||
if (isCollecting) return
|
applicationScope.launch {
|
||||||
isCollecting = true
|
|
||||||
lifecycleScope.launch {
|
|
||||||
tunnelManager.activeTunnels.collect {
|
|
||||||
updateTileState()
|
updateTileState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun updateTileState() {
|
fun updateTileState() = applicationScope.launch {
|
||||||
try {
|
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
|
||||||
val tunnels = appDataRepository.tunnels.getAll()
|
with(tunnelManager.activeTunnels().value) {
|
||||||
if (tunnels.isEmpty()) {
|
if (isNotEmpty()) return@launch updateTile(if (size == 1) first().tunName else getString(R.string.multiple), true)
|
||||||
setUnavailable()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val activeTunnels = tunnelManager.activeTunnels.value
|
|
||||||
.filter { it.value.status.isUpOrStarting() }
|
|
||||||
|
|
||||||
when {
|
|
||||||
activeTunnels.isNotEmpty() -> {
|
|
||||||
val activeIds = activeTunnels.map { it.key.id }
|
|
||||||
// TODO improvements would be needed to make this work well with toggling multiple tunnels
|
|
||||||
// this would be better managed elsewhere
|
|
||||||
WireGuardAutoTunnel.setLastActiveTunnels(activeIds)
|
|
||||||
updateTileForActiveTunnels(activeTunnels)
|
|
||||||
}
|
|
||||||
else -> updateTileForLastActiveTunnels()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
setUnavailable()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateTileForActiveTunnels(activeTunnels: Map<TunnelConf, TunnelState>) {
|
|
||||||
val tileName = when (activeTunnels.size) {
|
|
||||||
1 -> activeTunnels.keys.first().tunName
|
|
||||||
else -> getString(R.string.multiple)
|
|
||||||
}
|
|
||||||
updateTile(tileName, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun updateTileForLastActiveTunnels() {
|
|
||||||
val lastActiveIds = WireGuardAutoTunnel.getLastActiveTunnels()
|
|
||||||
when {
|
|
||||||
lastActiveIds.isEmpty() -> {
|
|
||||||
appDataRepository.getStartTunnelConfig()?.let { config ->
|
|
||||||
updateTile(config.tunName, false)
|
|
||||||
} ?: setUnavailable()
|
|
||||||
}
|
|
||||||
lastActiveIds.size > 1 -> updateTile(getString(R.string.multiple), false)
|
|
||||||
else -> {
|
|
||||||
val tunnelId = lastActiveIds.first()
|
|
||||||
appDataRepository.tunnels.getById(tunnelId)?.let { tunnel ->
|
|
||||||
updateTile(tunnel.tunName, false)
|
|
||||||
} ?: setUnavailable()
|
|
||||||
}
|
}
|
||||||
|
appDataRepository.getStartTunnelConfig()?.let {
|
||||||
|
updateTile(it.tunName, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
super.onClick()
|
super.onClick()
|
||||||
unlockAndRun {
|
unlockAndRun {
|
||||||
lifecycleScope.launch {
|
applicationScope.launch {
|
||||||
if (tunnelManager.activeTunnels.value.isNotEmpty()) return@launch tunnelManager.stopTunnel()
|
if (tunnelManager.activeTunnels().value.isNotEmpty()) return@launch tunnelManager.stopTunnel()
|
||||||
val lastActive = WireGuardAutoTunnel.getLastActiveTunnels()
|
|
||||||
if (lastActive.isEmpty()) {
|
|
||||||
appDataRepository.getStartTunnelConfig()?.let {
|
appDataRepository.getStartTunnelConfig()?.let {
|
||||||
tunnelManager.startTunnel(it)
|
tunnelManager.startTunnel(it)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
lastActive.forEach { id ->
|
|
||||||
appDataRepository.tunnels.getById(id)?.let {
|
|
||||||
tunnelManager.startTunnel(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,6 +127,4 @@ class TunnelControlTile : TileService(), LifecycleOwner {
|
|||||||
Timber.e(it)
|
Timber.e(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
override val lifecycle: Lifecycle
|
|
||||||
get() = lifecycleRegistry
|
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-14
@@ -2,12 +2,11 @@ package com.zaneschepke.wireguardautotunnel.core.shortcut
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
|
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -22,9 +21,6 @@ class ShortcutsActivity : ComponentActivity() {
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var serviceManager: ServiceManager
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var tunnelManager: TunnelManager
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
@ApplicationScope
|
@ApplicationScope
|
||||||
lateinit var applicationScope: CoroutineScope
|
lateinit var applicationScope: CoroutineScope
|
||||||
@@ -43,17 +39,17 @@ class ShortcutsActivity : ComponentActivity() {
|
|||||||
.firstOrNull { it.tunName == tunnelName }
|
.firstOrNull { it.tunName == tunnelName }
|
||||||
} ?: appDataRepository.getStartTunnelConfig()
|
} ?: appDataRepository.getStartTunnelConfig()
|
||||||
Timber.d("Shortcut action on name: ${tunnelConfig?.tunName}")
|
Timber.d("Shortcut action on name: ${tunnelConfig?.tunName}")
|
||||||
tunnelConfig?.let {
|
// tunnelConfig?.let {
|
||||||
when (intent.action) {
|
// when (intent.action) {
|
||||||
Action.START.name -> tunnelManager.startTunnel(it)
|
// Action.START.name -> tunnelService.get().startTunnel(it)
|
||||||
Action.STOP.name -> tunnelManager.stopTunnel()
|
// Action.STOP.name -> tunnelService.get().stopTunnel()
|
||||||
else -> Unit
|
// else -> Unit
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
AutoTunnelService::class.java.simpleName, LEGACY_AUTO_TUNNEL_SERVICE_NAME -> {
|
AutoTunnelService::class.java.simpleName, LEGACY_AUTO_TUNNEL_SERVICE_NAME -> {
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
Action.START.name -> serviceManager.startAutoTunnel()
|
Action.START.name -> serviceManager.startAutoTunnel(true)
|
||||||
Action.STOP.name -> serviceManager.stopAutoTunnel()
|
Action.STOP.name -> serviceManager.stopAutoTunnel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,204 +1,219 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||||
|
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider.Companion.CHECK_INTERVAL
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
|
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import kotlin.concurrent.thread
|
|
||||||
|
|
||||||
abstract class BaseTunnel(
|
open class BaseTunnel(
|
||||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||||
|
private val networkMonitor: NetworkMonitor,
|
||||||
private val appDataRepository: AppDataRepository,
|
private val appDataRepository: AppDataRepository,
|
||||||
private val serviceManager: ServiceManager,
|
private val serviceManager: ServiceManager,
|
||||||
|
private val notificationManager: NotificationManager,
|
||||||
) : TunnelProvider {
|
) : TunnelProvider {
|
||||||
|
|
||||||
private val activeTuns = MutableStateFlow<Map<TunnelConf, TunnelState>>(emptyMap())
|
internal val tunnels = MutableStateFlow<List<TunnelConf>>(emptyList())
|
||||||
private val tunThreads = ConcurrentHashMap<Int, Thread>()
|
|
||||||
override val activeTunnels = activeTuns.asStateFlow()
|
|
||||||
|
|
||||||
private val tunMutex = Mutex()
|
private val tunnelJobs = mutableMapOf<TunnelConf, Job>()
|
||||||
private val tunStatusMutex = Mutex()
|
|
||||||
|
|
||||||
private val isBouncing = AtomicBoolean(false)
|
private val isNetworkAvailable = AtomicBoolean(false)
|
||||||
|
|
||||||
abstract suspend fun startBackend(tunnel: TunnelConf)
|
init {
|
||||||
|
applicationScope.launch(ioDispatcher) {
|
||||||
|
launch {
|
||||||
|
startNetworkJob()
|
||||||
|
}
|
||||||
|
tunnels.collect { tuns ->
|
||||||
|
val previousTuns = tunnelJobs.keys.toSet()
|
||||||
|
val newTuns = tuns - previousTuns
|
||||||
|
val removedItems = previousTuns - tuns.toSet()
|
||||||
|
|
||||||
abstract fun stopBackend(tunnel: TunnelConf)
|
newTuns.forEach { tun ->
|
||||||
|
Timber.d("Starting tunnel jobs for tun ${tun.name}")
|
||||||
override suspend fun clearError(tunnelConf: TunnelConf) = updateTunnelStatus(tunnelConf, TunnelStatus.Down)
|
tunnelJobs[tun] = startTunnelJobs(tun)
|
||||||
|
|
||||||
override fun hasVpnPermission(): Boolean {
|
|
||||||
return serviceManager.hasVpnPermission()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected suspend fun updateTunnelStatus(tunnelConf: TunnelConf, state: TunnelStatus? = null, stats: TunnelStatistics? = null) {
|
removedItems.forEach { tun ->
|
||||||
tunStatusMutex.withLock {
|
tunnelJobs[tun]?.cancelWithMessage("Canceling tunnel jobs for tunnel: ${tun.name}")
|
||||||
activeTuns.update { current ->
|
tunnelJobs.remove(tun)
|
||||||
val originalConf = current.getKeyById(tunnelConf.id) ?: tunnelConf
|
|
||||||
val existingState = current.getValueById(tunnelConf.id) ?: TunnelState()
|
|
||||||
val newState = state ?: existingState.status
|
|
||||||
if (newState == TunnelStatus.Down) {
|
|
||||||
Timber.d("Removing tunnel ${tunnelConf.id} from activeTunnels as state is DOWN")
|
|
||||||
current - originalConf
|
|
||||||
} else if (existingState.status == newState && stats == null) {
|
|
||||||
Timber.d("Skipping redundant state update for ${tunnelConf.id}: $newState")
|
|
||||||
current
|
|
||||||
} else {
|
|
||||||
val updated = existingState.copy(
|
|
||||||
status = newState,
|
|
||||||
statistics = stats ?: existingState.statistics,
|
|
||||||
)
|
|
||||||
current + (originalConf to updated)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun stopActiveTunnels() {
|
|
||||||
activeTunnels.value.forEach { (config, state) ->
|
|
||||||
if (state.status.isUpOrStarting()) {
|
|
||||||
stopTunnel(config)
|
|
||||||
delay(300)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun configureTunnelCallbacks(tunnelConf: TunnelConf) {
|
|
||||||
Timber.d("Configuring TunnelConf instance: ${tunnelConf.hashCode()}")
|
|
||||||
|
|
||||||
tunnelConf.setStateChangeCallback { state ->
|
|
||||||
Timber.d("State change callback triggered for tunnel ${tunnelConf.id}: ${tunnelConf.tunName} with state $state at ${System.currentTimeMillis()}")
|
|
||||||
when (state) {
|
|
||||||
is Tunnel.State -> applicationScope.launch { updateTunnelStatus(tunnelConf, state.asTunnelState()) }
|
|
||||||
is org.amnezia.awg.backend.Tunnel.State -> applicationScope.launch { updateTunnelStatus(tunnelConf, state.asTunnelState()) }
|
|
||||||
}
|
}
|
||||||
serviceManager.updateTunnelTile()
|
serviceManager.updateTunnelTile()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun updateTunnelStatistics(tunnel: TunnelConf) {
|
private fun startTunnelJobs(tunnel: TunnelConf) = applicationScope.launch(ioDispatcher) {
|
||||||
val stats = getStatistics(tunnel)
|
launch {
|
||||||
updateTunnelStatus(tunnel, null, stats)
|
startTunnelStatisticsJob(tunnel)
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
startPingJob(tunnel)
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
startTunnelConfigChangeJob(tunnel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun bounceTunnel(tunnelConf: TunnelConf) {
|
||||||
|
if (tunnels.value.any { it.id == tunnelConf.id }) {
|
||||||
|
toggleTunnel(tunnelConf, TunnelStatus.DOWN)
|
||||||
|
toggleTunnel(tunnelConf, TunnelStatus.UP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun runningTunnelNames(): Set<String> {
|
||||||
|
return emptySet()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun activeTunnels(): StateFlow<List<TunnelConf>> {
|
||||||
|
return tunnels.asStateFlow()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun startTunnel(tunnelConf: TunnelConf) {
|
override suspend fun startTunnel(tunnelConf: TunnelConf) {
|
||||||
if (activeTuns.exists(tunnelConf.id) || tunThreads.containsKey(tunnelConf.id)) return
|
if (tunnels.value.any { it.id == tunnelConf.id }) return Timber.w("Tunnel already running")
|
||||||
// stop active tunnels if we are userspace
|
serviceManager.startBackgroundService(tunnelConf)
|
||||||
if (this@BaseTunnel is UserspaceTunnel) stopActiveTunnels()
|
appDataRepository.tunnels.save(tunnelConf.copy(isActive = true))
|
||||||
tunMutex.withLock {
|
}
|
||||||
// use thread to interrupt java backend if stuck (like in dns resolution)
|
|
||||||
tunThreads += tunnelConf.id to thread {
|
override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
|
||||||
runBlocking {
|
}
|
||||||
try {
|
|
||||||
Timber.d("Starting tunnel ${tunnelConf.id}...")
|
open suspend fun toggleTunnel(tunnelConf: TunnelConf, state: TunnelStatus) {
|
||||||
startTunnelInner(tunnelConf)
|
}
|
||||||
Timber.d("Started complete for tunnel ${tunnelConf.name}...")
|
|
||||||
} catch (e: BackendError) {
|
open suspend fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
||||||
Timber.e(e, "Failed to start tunnel ${tunnelConf.name} userspace")
|
throw NotImplementedError("Get statistics not implemented in base class")
|
||||||
updateTunnelStatus(tunnelConf, TunnelStatus.Error(e))
|
}
|
||||||
} catch (e: InterruptedException) {
|
|
||||||
Timber.i("Tunnel start has been interrupted as ${tunnelConf.name} failed to start")
|
internal suspend fun onTunnelStop(tunnelConf: TunnelConf) {
|
||||||
|
appDataRepository.tunnels.save(tunnelConf.copy(isActive = false))
|
||||||
|
removeFromActiveTunnels(tunnelConf)
|
||||||
|
if (tunnels.value.isEmpty()) serviceManager.stopBackgroundService()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal suspend fun stopAllTunnels() {
|
||||||
|
tunnels.value.forEach {
|
||||||
|
stopTunnel(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun addToActiveTunnels(conf: TunnelConf) {
|
||||||
|
tunnels.update {
|
||||||
|
it.toMutableList().apply {
|
||||||
|
add(conf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun startTunnelInner(tunnelConf: TunnelConf) {
|
private fun removeFromActiveTunnels(conf: TunnelConf) {
|
||||||
configureTunnelCallbacks(tunnelConf)
|
tunnels.update {
|
||||||
Timber.d("Started backend for tunnel ${tunnelConf.id}...")
|
it.toMutableList().apply {
|
||||||
startBackend(tunnelConf)
|
remove(conf)
|
||||||
updateTunnelStatus(tunnelConf, TunnelStatus.Up)
|
|
||||||
Timber.d("DONE for tun ${tunnelConf.id}...")
|
|
||||||
saveTunnelActiveState(tunnelConf, true)
|
|
||||||
serviceManager.startTunnelForegroundService()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun saveTunnelActiveState(tunnelConf: TunnelConf, active: Boolean) {
|
|
||||||
val tunnelCopy = tunnelConf.copyWithCallback(isActive = active)
|
|
||||||
appDataRepository.tunnels.save(tunnelCopy)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun stopTunnel(tunnelConf: TunnelConf?, reason: TunnelStatus.StopReason) {
|
|
||||||
if (tunnelConf == null) return stopActiveTunnels()
|
|
||||||
tunMutex.withLock {
|
|
||||||
try {
|
|
||||||
val stuckStarting = activeTuns.isStarting(tunnelConf.id)
|
|
||||||
handleTunnelThreadCleanup(tunnelConf)
|
|
||||||
if (stuckStarting) return Timber.d("Stuck in starting, so just shutting down tunnel thread")
|
|
||||||
updateTunnelStatus(tunnelConf, TunnelStatus.Stopping(reason))
|
|
||||||
stopTunnelInner(tunnelConf)
|
|
||||||
} catch (e: BackendError) {
|
|
||||||
Timber.e(e, "Failed to stop tunnel ${tunnelConf.id}")
|
|
||||||
updateTunnelStatus(tunnelConf, TunnelStatus.Error(e))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun stopTunnelInner(tunnelConf: TunnelConf) {
|
private suspend fun startNetworkJob() = coroutineScope {
|
||||||
val tunnel = activeTuns.findTunnel(tunnelConf.id) ?: return
|
networkMonitor.status.distinctUntilChanged().collect {
|
||||||
stopBackend(tunnel)
|
isNetworkAvailable.set(!it.allOffline)
|
||||||
saveTunnelActiveState(tunnelConf, false)
|
}
|
||||||
removeActiveTunnel(tunnel)
|
|
||||||
handleServiceChangesOnStop()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleServiceChangesOnStop() {
|
private suspend fun startPingJob(tunnel: TunnelConf) = coroutineScope {
|
||||||
if (activeTuns.value.isEmpty() && !isBouncing.get()) return serviceManager.stopTunnelForegroundService()
|
while (isActive) {
|
||||||
|
if (isNetworkAvailable.get() && tunnel.isActive) {
|
||||||
|
val pingResult = tunnel.pingTunnel(ioDispatcher)
|
||||||
|
handlePingResult(tunnel, pingResult)
|
||||||
|
}
|
||||||
|
delay(CHECK_INTERVAL)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleTunnelThreadCleanup(tunnel: TunnelConf) {
|
private suspend fun handlePingResult(tunnel: TunnelConf, pingResult: List<Boolean>) {
|
||||||
Timber.d("Cleaning up thread for ${tunnel.name}")
|
if (pingResult.contains(false)) {
|
||||||
try {
|
if (isNetworkAvailable.get()) {
|
||||||
tunThreads[tunnel.id]?.let {
|
Timber.i("Ping result: target was not reachable, bouncing the tunnel")
|
||||||
if (it.state != Thread.State.TERMINATED) {
|
bounceTunnel(tunnel)
|
||||||
it.interrupt()
|
delay(tunnel.pingCooldown ?: Constants.PING_COOLDOWN)
|
||||||
updateTunnelStatus(tunnel, TunnelStatus.Down)
|
|
||||||
} else {
|
} else {
|
||||||
Timber.d("Thread already terminated")
|
Timber.i("Ping result: target was not reachable, but no network available")
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
} catch (e: Exception) {
|
Timber.i("Ping result: all ping targets were reached successfully")
|
||||||
Timber.e(e, "Failed to stop tunnel thread for ${tunnel.name}")
|
|
||||||
}
|
|
||||||
Timber.d("Removing thread for ${tunnel.name}")
|
|
||||||
tunThreads -= tunnel.id
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun removeActiveTunnel(tunnelConf: TunnelConf) {
|
|
||||||
activeTuns.update { current ->
|
|
||||||
current.toMutableMap().apply { remove(tunnelConf) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun bounceTunnel(tunnelConf: TunnelConf, reason: TunnelStatus.StopReason) {
|
internal fun handleBackendThrowable(backendError: BackendError) {
|
||||||
Timber.i("Bounce tunnel ${tunnelConf.name}")
|
val message = when (backendError) {
|
||||||
isBouncing.set(true)
|
BackendError.Config -> StringValue.StringResource(R.string.start_failed_config)
|
||||||
stopTunnel(tunnelConf, reason)
|
BackendError.DNS -> StringValue.StringResource(R.string.dns_error)
|
||||||
startTunnel(tunnelConf)
|
BackendError.Unauthorized -> StringValue.StringResource(R.string.unauthorized)
|
||||||
isBouncing.set(false)
|
}
|
||||||
|
if (WireGuardAutoTunnel.isForeground()) {
|
||||||
|
SnackbarController.showMessage(message)
|
||||||
|
} else {
|
||||||
|
notificationManager.show(
|
||||||
|
NotificationManager.VPN_NOTIFICATION_ID,
|
||||||
|
notificationManager.createNotification(
|
||||||
|
WireGuardNotification.NotificationChannels.VPN,
|
||||||
|
title = StringValue.StringResource(R.string.tunne_start_failed_title),
|
||||||
|
description = message,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun runningTunnelNames(): Set<String> = activeTuns.value.keys.map { it.tunName }.toSet()
|
private suspend fun startTunnelConfigChangeJob(tunnel: TunnelConf) = coroutineScope {
|
||||||
|
appDataRepository.tunnels.flow.collect { storageTuns ->
|
||||||
|
storageTuns.firstOrNull { it.id == tunnel.id }?.let { storageTun ->
|
||||||
|
if (tunnel.isQuickConfigChanged(storageTun) || tunnel.isPingConfigMatching(storageTun)) {
|
||||||
|
bounceTunnel(tunnel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun startTunnelStatisticsJob(tunnel: TunnelConf) = coroutineScope {
|
||||||
|
while (isActive) {
|
||||||
|
val stats = getStatistics(tunnel)
|
||||||
|
tunnel.state.update {
|
||||||
|
it.copy(statistics = stats)
|
||||||
|
}
|
||||||
|
delay(CHECK_INTERVAL)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
|
|
||||||
fun Map<TunnelConf, TunnelState>.allDown(): Boolean {
|
|
||||||
return this.all { it.value.status.isDown() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Map<TunnelConf, TunnelState>.hasActive(): Boolean {
|
|
||||||
return this.any { it.value.status.isUp() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Map<TunnelConf, TunnelState>.getValueById(id: Int): TunnelState? {
|
|
||||||
val key = this.keys.find { it.id == id }
|
|
||||||
return key?.let { this@getValueById[it] }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Map<TunnelConf, TunnelState>.getKeyById(id: Int): TunnelConf? {
|
|
||||||
return this.keys.find { it.id == id }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Map<TunnelConf, TunnelState>.isUp(tunnelConf: TunnelConf): Boolean {
|
|
||||||
return this.getValueById(tunnelConf.id)?.status?.isUp() ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MutableStateFlow<Map<TunnelConf, TunnelState>>.exists(id: Int): Boolean {
|
|
||||||
return this.value.any { it.key.id == id }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MutableStateFlow<Map<TunnelConf, TunnelState>>.isUp(id: Int): Boolean {
|
|
||||||
return this.value.any { it.key.id == id && it.value.status == TunnelStatus.Up }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MutableStateFlow<Map<TunnelConf, TunnelState>>.isStarting(id: Int): Boolean {
|
|
||||||
return this.value.any { it.key.id == id && it.value.status == TunnelStatus.Starting }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MutableStateFlow<Map<TunnelConf, TunnelState>>.findTunnel(id: Int): TunnelConf? {
|
|
||||||
return this.value.keys.find { it.id == id }
|
|
||||||
}
|
|
||||||
|
|
||||||
private val URL_PATTERN = Regex(
|
|
||||||
"""^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}:[0-9]{1,5}$""",
|
|
||||||
)
|
|
||||||
|
|
||||||
fun String.isUrl(): Boolean {
|
|
||||||
return URL_PATTERN.matches(this)
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,8 @@ package com.zaneschepke.wireguardautotunnel.core.tunnel
|
|||||||
import com.wireguard.android.backend.Backend
|
import com.wireguard.android.backend.Backend
|
||||||
import com.wireguard.android.backend.BackendException
|
import com.wireguard.android.backend.BackendException
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
@@ -15,6 +17,7 @@ import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics
|
|||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
|
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -23,44 +26,57 @@ class KernelTunnel @Inject constructor(
|
|||||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||||
serviceManager: ServiceManager,
|
serviceManager: ServiceManager,
|
||||||
appDataRepository: AppDataRepository,
|
appDataRepository: AppDataRepository,
|
||||||
|
notificationManager: NotificationManager,
|
||||||
private val backend: Backend,
|
private val backend: Backend,
|
||||||
) : BaseTunnel(ioDispatcher, applicationScope, appDataRepository, serviceManager) {
|
networkMonitor: NetworkMonitor,
|
||||||
|
) : BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) {
|
||||||
|
|
||||||
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
|
override suspend fun startTunnel(tunnelConf: TunnelConf) {
|
||||||
return try {
|
withContext(ioDispatcher) {
|
||||||
WireGuardStatistics(backend.getStatistics(tunnelConf))
|
super.startTunnel(tunnelConf)
|
||||||
} catch (e: Exception) {
|
runCatching {
|
||||||
Timber.e(e)
|
backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toWgConfig())
|
||||||
null
|
addToActiveTunnels(tunnelConf)
|
||||||
|
}.onFailure {
|
||||||
|
onTunnelStop(tunnelConf)
|
||||||
|
if (it is BackendException) {
|
||||||
|
handleBackendThrowable(it.toBackendError())
|
||||||
|
} else {
|
||||||
|
Timber.e(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun startBackend(tunnel: TunnelConf) {
|
override suspend fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
||||||
try {
|
return WireGuardStatistics(backend.getStatistics(tunnelConf))
|
||||||
updateTunnelStatus(tunnel, TunnelStatus.Starting)
|
}
|
||||||
backend.setState(tunnel, Tunnel.State.UP, tunnel.toWgConfig())
|
|
||||||
} catch (e: BackendException) {
|
override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
|
||||||
throw e.toBackendError()
|
withContext(ioDispatcher) {
|
||||||
|
val tunnel = tunnels.value.firstOrNull { it.id == tunnelConf?.id }
|
||||||
|
runCatching {
|
||||||
|
tunnel?.let {
|
||||||
|
backend.setState(it, Tunnel.State.DOWN, it.toWgConfig())
|
||||||
|
onTunnelStop(it)
|
||||||
|
} ?: stopAllTunnels()
|
||||||
|
}.onFailure {
|
||||||
|
Timber.e(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stopBackend(tunnel: TunnelConf) {
|
override suspend fun toggleTunnel(tunnelConf: TunnelConf, status: TunnelStatus) {
|
||||||
Timber.i("Stopping tunnel ${tunnel.id} kernel")
|
when (status) {
|
||||||
try {
|
TunnelStatus.UP -> backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toWgConfig())
|
||||||
backend.setState(tunnel, Tunnel.State.DOWN, tunnel.toWgConfig())
|
TunnelStatus.DOWN -> backend.setState(tunnelConf, Tunnel.State.DOWN, tunnelConf.toWgConfig())
|
||||||
} catch (e: BackendException) {
|
|
||||||
throw e.toBackendError()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
||||||
Timber.w("Not yet implemented for kernel")
|
Timber.w("Not yet implemented for kernel")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getBackendState(): BackendState {
|
|
||||||
return BackendState.INACTIVE
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun runningTunnelNames(): Set<String> {
|
override suspend fun runningTunnelNames(): Set<String> {
|
||||||
return backend.runningTunnelNames
|
return backend.runningTunnelNames
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-61
@@ -4,21 +4,20 @@ import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
|||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.di.Kernel
|
import com.zaneschepke.wireguardautotunnel.di.Kernel
|
||||||
import com.zaneschepke.wireguardautotunnel.di.Userspace
|
import com.zaneschepke.wireguardautotunnel.di.Userspace
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
import com.zaneschepke.wireguardautotunnel.util.extensions.withData
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class TunnelManager @Inject constructor(
|
class TunnelManager @Inject constructor(
|
||||||
@@ -29,85 +28,71 @@ class TunnelManager @Inject constructor(
|
|||||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||||
) : TunnelProvider {
|
) : TunnelProvider {
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
val appSettings: StateFlow<AppSettings?> = appDataRepository.settings.flow.stateIn(
|
||||||
private val tunnelProviderFlow = appDataRepository.settings.flow
|
|
||||||
.filterNotNull()
|
|
||||||
.flatMapLatest { settings ->
|
|
||||||
MutableStateFlow(if (settings.isKernelEnabled) kernelTunnel else userspaceTunnel)
|
|
||||||
}
|
|
||||||
.stateIn(
|
|
||||||
scope = applicationScope.plus(ioDispatcher),
|
scope = applicationScope.plus(ioDispatcher),
|
||||||
started = SharingStarted.Eagerly,
|
started = SharingStarted.Eagerly,
|
||||||
initialValue = userspaceTunnel,
|
initialValue = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
override suspend fun activeTunnels(): StateFlow<List<TunnelConf>> {
|
||||||
override val activeTunnels = appDataRepository.settings.flow
|
return withContext(ioDispatcher) {
|
||||||
.filterNotNull()
|
appSettings.filterNotNull().first().let {
|
||||||
.flatMapLatest { settings ->
|
if (it.isKernelEnabled) return@withContext kernelTunnel.activeTunnels()
|
||||||
if (settings.isKernelEnabled) {
|
userspaceTunnel.activeTunnels()
|
||||||
kernelTunnel.activeTunnels
|
|
||||||
} else {
|
|
||||||
userspaceTunnel.activeTunnels
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.stateIn(
|
|
||||||
scope = applicationScope.plus(ioDispatcher),
|
|
||||||
started = SharingStarted.Eagerly,
|
|
||||||
initialValue = emptyMap(),
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun hasVpnPermission(): Boolean {
|
|
||||||
return userspaceTunnel.hasVpnPermission()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun clearError(tunnelConf: TunnelConf) {
|
|
||||||
tunnelProviderFlow.value.clearError(tunnelConf)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun updateTunnelStatistics(tunnel: TunnelConf) {
|
|
||||||
tunnelProviderFlow.value.updateTunnelStatistics(tunnel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun startTunnel(tunnelConf: TunnelConf) {
|
override suspend fun startTunnel(tunnelConf: TunnelConf) {
|
||||||
tunnelProviderFlow.value.startTunnel(tunnelConf)
|
appSettings.withData {
|
||||||
|
if (it.isKernelEnabled) return@withData kernelTunnel.startTunnel(tunnelConf)
|
||||||
|
userspaceTunnel.startTunnel(tunnelConf)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun stopTunnel(tunnelConf: TunnelConf?, reason: TunnelStatus.StopReason) {
|
override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
|
||||||
tunnelProviderFlow.value.stopTunnel(tunnelConf)
|
appSettings.withData {
|
||||||
|
if (it.isKernelEnabled) return@withData kernelTunnel.stopTunnel(tunnelConf)
|
||||||
|
userspaceTunnel.stopTunnel(tunnelConf)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun bounceTunnel(tunnelConf: TunnelConf, reason: TunnelStatus.StopReason) {
|
override suspend fun bounceTunnel(tunnelConf: TunnelConf) {
|
||||||
tunnelProviderFlow.value.bounceTunnel(tunnelConf)
|
appSettings.withData {
|
||||||
|
if (it.isKernelEnabled) return@withData kernelTunnel.stopTunnel(tunnelConf)
|
||||||
|
userspaceTunnel.stopTunnel(tunnelConf)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
||||||
tunnelProviderFlow.value.setBackendState(backendState, allowedIps)
|
appSettings.withData {
|
||||||
|
if (it.isKernelEnabled) return@withData kernelTunnel.setBackendState(backendState, allowedIps)
|
||||||
|
userspaceTunnel.setBackendState(backendState, allowedIps)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getBackendState(): BackendState {
|
|
||||||
return tunnelProviderFlow.value.getBackendState()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun runningTunnelNames(): Set<String> {
|
override suspend fun runningTunnelNames(): Set<String> {
|
||||||
return tunnelProviderFlow.value.runningTunnelNames()
|
appSettings.filterNotNull().first().let {
|
||||||
|
if (it.isKernelEnabled) return kernelTunnel.runningTunnelNames()
|
||||||
|
return userspaceTunnel.runningTunnelNames()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
|
suspend fun restorePreviousState() {
|
||||||
return tunnelProviderFlow.value.getStatistics(tunnelConf)
|
withContext(ioDispatcher) {
|
||||||
}
|
with(appDataRepository.settings.get()) {
|
||||||
|
if (isRestoreOnBootEnabled) {
|
||||||
fun restorePreviousState() = applicationScope.launch(ioDispatcher) {
|
|
||||||
val settings = appDataRepository.settings.get()
|
|
||||||
if (settings.isRestoreOnBootEnabled) {
|
|
||||||
val previouslyActiveTuns = appDataRepository.tunnels.getActive()
|
val previouslyActiveTuns = appDataRepository.tunnels.getActive()
|
||||||
val tunsToStart = previouslyActiveTuns.filterNot { tun -> activeTunnels.value.any { tun.id == it.key.id } }
|
// handle kernel mode
|
||||||
if (settings.isKernelEnabled) {
|
val tunsToStart = previouslyActiveTuns.filterNot { tun -> activeTunnels().value.any { tun.id == it.id } }
|
||||||
return@launch tunsToStart.forEach {
|
if (isKernelEnabled) {
|
||||||
|
return@withContext tunsToStart.forEach {
|
||||||
startTunnel(it)
|
startTunnel(it)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
tunsToStart.firstOrNull()?.let { startTunnel(it) }
|
// handle userspace
|
||||||
|
if (activeTunnels().value.isEmpty()) tunsToStart.firstOrNull()?.let { startTunnel(it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-12
@@ -2,21 +2,23 @@ package com.zaneschepke.wireguardautotunnel.core.tunnel
|
|||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
interface TunnelProvider {
|
interface TunnelProvider {
|
||||||
|
|
||||||
|
suspend fun activeTunnels(): StateFlow<List<TunnelConf>>
|
||||||
|
|
||||||
suspend fun startTunnel(tunnelConf: TunnelConf)
|
suspend fun startTunnel(tunnelConf: TunnelConf)
|
||||||
suspend fun stopTunnel(tunnelConf: TunnelConf? = null, reason: TunnelStatus.StopReason = TunnelStatus.StopReason.USER)
|
|
||||||
suspend fun bounceTunnel(tunnelConf: TunnelConf, reason: TunnelStatus.StopReason = TunnelStatus.StopReason.USER)
|
suspend fun stopTunnel(tunnelConf: TunnelConf? = null)
|
||||||
fun setBackendState(backendState: BackendState, allowedIps: Collection<String>)
|
|
||||||
fun getBackendState(): BackendState
|
suspend fun bounceTunnel(tunnelConf: TunnelConf)
|
||||||
|
|
||||||
|
suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>)
|
||||||
|
|
||||||
suspend fun runningTunnelNames(): Set<String>
|
suspend fun runningTunnelNames(): Set<String>
|
||||||
fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics?
|
|
||||||
val activeTunnels: StateFlow<Map<TunnelConf, TunnelState>>
|
companion object {
|
||||||
fun hasVpnPermission(): Boolean
|
const val CHECK_INTERVAL = 1_000L
|
||||||
suspend fun clearError(tunnelConf: TunnelConf)
|
}
|
||||||
suspend fun updateTunnelStatistics(tunnel: TunnelConf)
|
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-63
@@ -1,5 +1,8 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||||
|
|
||||||
|
import com.wireguard.android.backend.BackendException
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
@@ -10,98 +13,71 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
|||||||
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
|
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState
|
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
|
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.amnezia.awg.backend.Backend
|
import org.amnezia.awg.backend.Backend
|
||||||
import org.amnezia.awg.backend.BackendException
|
|
||||||
import org.amnezia.awg.backend.Tunnel
|
import org.amnezia.awg.backend.Tunnel
|
||||||
import org.amnezia.awg.config.Config
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.jvm.optionals.getOrNull
|
|
||||||
|
|
||||||
class UserspaceTunnel @Inject constructor(
|
class UserspaceTunnel @Inject constructor(
|
||||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||||
val serviceManager: ServiceManager,
|
serviceManager: ServiceManager,
|
||||||
val appDataRepository: AppDataRepository,
|
appDataRepository: AppDataRepository,
|
||||||
|
notificationManager: NotificationManager,
|
||||||
private val backend: Backend,
|
private val backend: Backend,
|
||||||
) : BaseTunnel(ioDispatcher, applicationScope, appDataRepository, serviceManager) {
|
networkMonitor: NetworkMonitor,
|
||||||
|
) : TunnelProvider, BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) {
|
||||||
|
|
||||||
private var previousBackendState: Pair<BackendState, Boolean>? = null
|
override suspend fun startTunnel(tunnelConf: TunnelConf) {
|
||||||
|
withContext(ioDispatcher) {
|
||||||
override suspend fun startBackend(tunnel: TunnelConf) {
|
super.startTunnel(tunnelConf)
|
||||||
try {
|
runCatching {
|
||||||
updateTunnelStatus(tunnel, TunnelStatus.Starting)
|
backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toAmConfig())
|
||||||
val amConfig = tunnel.toAmConfig()
|
addToActiveTunnels(tunnelConf)
|
||||||
handleVpnKillSwitchWithDomainEndpoints(amConfig)
|
}.onFailure {
|
||||||
backend.setState(tunnel, Tunnel.State.UP, amConfig)
|
onTunnelStop(tunnelConf)
|
||||||
} catch (e: BackendException) {
|
if (it is BackendException) {
|
||||||
Timber.e(e, "Failed to start up backend for tunnel ${tunnel.name}")
|
handleBackendThrowable(it.toBackendError())
|
||||||
throw e.toBackendError()
|
} else {
|
||||||
|
Timber.e(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stopBackend(tunnel: TunnelConf) {
|
override suspend fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
||||||
Timber.i("Stopping tunnel ${tunnel.name} userspace")
|
return AmneziaStatistics(backend.getStatistics(tunnelConf))
|
||||||
try {
|
|
||||||
backend.setState(tunnel, Tunnel.State.DOWN, tunnel.toAmConfig())
|
|
||||||
} catch (e: BackendException) {
|
|
||||||
Timber.e(e, "Failed to stop tunnel ${tunnel.id}")
|
|
||||||
throw e.toBackendError()
|
|
||||||
}
|
|
||||||
handlePreviouslyEnabledVpnKillSwitch()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// stop vpn kill switch if we need to resolve DNS for peer endpoints
|
override suspend fun toggleTunnel(tunnelConf: TunnelConf, status: TunnelStatus) {
|
||||||
private suspend fun handleVpnKillSwitchWithDomainEndpoints(config: Config) {
|
when (status) {
|
||||||
if (config.peers.any { it.endpoint.getOrNull()?.toString()?.isUrl() == true } &&
|
TunnelStatus.UP -> backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toAmConfig())
|
||||||
backend.backendState.asBackendState() == BackendState.KILL_SWITCH_ACTIVE
|
TunnelStatus.DOWN -> backend.setState(tunnelConf, Tunnel.State.DOWN, tunnelConf.toAmConfig())
|
||||||
) {
|
|
||||||
val bypassLan = appDataRepository.settings.get().isLanOnKillSwitchEnabled
|
|
||||||
previousBackendState = Pair(BackendState.KILL_SWITCH_ACTIVE, bypassLan)
|
|
||||||
setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// restore vpn kill switch if needed
|
override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
|
||||||
private fun handlePreviouslyEnabledVpnKillSwitch() {
|
withContext(ioDispatcher) {
|
||||||
// let auto tunnel handle this if it is active
|
runCatching {
|
||||||
if (!serviceManager.autoTunnelActive.value) {
|
tunnels.value.firstOrNull { it.id == tunnelConf?.id }?.let {
|
||||||
previousBackendState?.let { (state, lanEnabled) ->
|
backend.setState(it, Tunnel.State.DOWN, it.toAmConfig())
|
||||||
Timber.d("Restoring kill switch configuration")
|
onTunnelStop(it)
|
||||||
val lan = if (lanEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS else emptyList()
|
} ?: stopAllTunnels()
|
||||||
backend.setBackendState(state.asAmBackendState(), lan)
|
}.onFailure {
|
||||||
|
Timber.e(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
previousBackendState = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
||||||
Timber.d("Setting backend state: $backendState with allowedIps: $allowedIps")
|
|
||||||
try {
|
|
||||||
backend.setBackendState(backendState.asAmBackendState(), allowedIps)
|
backend.setBackendState(backendState.asAmBackendState(), allowedIps)
|
||||||
} catch (e: BackendException) {
|
|
||||||
throw e.toBackendError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getBackendState(): BackendState {
|
|
||||||
return backend.backendState.asBackendState()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun runningTunnelNames(): Set<String> {
|
override suspend fun runningTunnelNames(): Set<String> {
|
||||||
return backend.runningTunnelNames
|
return backend.runningTunnelNames
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
|
|
||||||
return try {
|
|
||||||
AmneziaStatistics(backend.getStatistics(tunnelConf))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "Failed to get stats for ${tunnelConf.tunName}")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ class ServiceWorker @AssistedInject constructor(
|
|||||||
override suspend fun doWork(): Result = withContext(ioDispatcher) {
|
override suspend fun doWork(): Result = withContext(ioDispatcher) {
|
||||||
Timber.i("Service worker started")
|
Timber.i("Service worker started")
|
||||||
with(appDataRepository.settings.get()) {
|
with(appDataRepository.settings.get()) {
|
||||||
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@with serviceManager.startAutoTunnel()
|
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@with serviceManager.startAutoTunnel(true)
|
||||||
if (tunnelManager.activeTunnels.value.isEmpty()) tunnelManager.restorePreviousState()
|
if (tunnelManager.activeTunnels().value.isEmpty()) tunnelManager.restorePreviousState()
|
||||||
}
|
}
|
||||||
Result.success()
|
Result.success()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data
|
package com.zaneschepke.wireguardautotunnel.data
|
||||||
|
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
class DatabaseListConverters {
|
class DatabaseListConverters {
|
||||||
|
|||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class AppDataRoomRepository
|
class AppDataRoomRepository
|
||||||
|
|||||||
+1
-1
@@ -1,8 +1,8 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager
|
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|||||||
+1
-1
@@ -1,10 +1,10 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao
|
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|||||||
+1
-7
@@ -1,10 +1,10 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
|
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
|
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
@@ -30,12 +30,6 @@ class RoomTunnelRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun saveAll(tunnelConfs: List<TunnelConf>) {
|
|
||||||
withContext(ioDispatcher) {
|
|
||||||
tunnelConfigDao.saveAll(tunnelConfs.map(TunnelConfig::from))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun updatePrimaryTunnel(tunnelConf: TunnelConf?) {
|
override suspend fun updatePrimaryTunnel(tunnelConf: TunnelConf?) {
|
||||||
withContext(ioDispatcher) {
|
withContext(ioDispatcher) {
|
||||||
tunnelConfigDao.resetPrimaryTunnel()
|
tunnelConfigDao.resetPrimaryTunnel()
|
||||||
|
|||||||
@@ -4,17 +4,17 @@ import android.content.Context
|
|||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||||
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
|
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
|
||||||
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao
|
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao
|
||||||
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
|
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRoomRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRoomRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.DataStoreAppStateRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.DataStoreAppStateRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.RoomSettingsRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.RoomSettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.RoomTunnelRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.RoomTunnelRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.di
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.network.InternetConnectivityMonitor
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
abstract class ServiceModule {
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun provideInternetConnectivityService(wifiService: InternetConnectivityMonitor): NetworkMonitor
|
||||||
|
}
|
||||||
@@ -4,15 +4,14 @@ import android.content.Context
|
|||||||
import com.wireguard.android.backend.WgQuickBackend
|
import com.wireguard.android.backend.WgQuickBackend
|
||||||
import com.wireguard.android.util.RootShell
|
import com.wireguard.android.util.RootShell
|
||||||
import com.wireguard.android.util.ToolsInstaller
|
import com.wireguard.android.util.ToolsInstaller
|
||||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
|
||||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.KernelTunnel
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.KernelTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.UserspaceTunnel
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.UserspaceTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
|
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
@@ -20,7 +19,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import org.amnezia.awg.backend.Backend
|
import org.amnezia.awg.backend.Backend
|
||||||
import org.amnezia.awg.backend.GoBackend
|
import org.amnezia.awg.backend.GoBackend
|
||||||
import org.amnezia.awg.backend.RootTunnelActionHandler
|
import org.amnezia.awg.backend.RootTunnelActionHandler
|
||||||
@@ -66,9 +64,11 @@ class TunnelModule {
|
|||||||
@ApplicationScope applicationScope: CoroutineScope,
|
@ApplicationScope applicationScope: CoroutineScope,
|
||||||
serviceManager: ServiceManager,
|
serviceManager: ServiceManager,
|
||||||
appDataRepository: AppDataRepository,
|
appDataRepository: AppDataRepository,
|
||||||
|
networkMonitor: NetworkMonitor,
|
||||||
|
notificationManager: NotificationManager,
|
||||||
backend: com.wireguard.android.backend.Backend,
|
backend: com.wireguard.android.backend.Backend,
|
||||||
): TunnelProvider {
|
): TunnelProvider {
|
||||||
return KernelTunnel(ioDispatcher, applicationScope, serviceManager, appDataRepository, backend)
|
return KernelTunnel(ioDispatcher, applicationScope, serviceManager, appDataRepository, notificationManager, backend, networkMonitor)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@@ -79,9 +79,11 @@ class TunnelModule {
|
|||||||
@ApplicationScope applicationScope: CoroutineScope,
|
@ApplicationScope applicationScope: CoroutineScope,
|
||||||
serviceManager: ServiceManager,
|
serviceManager: ServiceManager,
|
||||||
appDataRepository: AppDataRepository,
|
appDataRepository: AppDataRepository,
|
||||||
|
notificationManager: NotificationManager,
|
||||||
|
networkMonitor: NetworkMonitor,
|
||||||
backend: Backend,
|
backend: Backend,
|
||||||
): TunnelProvider {
|
): TunnelProvider {
|
||||||
return UserspaceTunnel(ioDispatcher, applicationScope, serviceManager, appDataRepository, backend)
|
return UserspaceTunnel(ioDispatcher, applicationScope, serviceManager, appDataRepository, notificationManager, backend, networkMonitor)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@@ -96,21 +98,13 @@ class TunnelModule {
|
|||||||
return TunnelManager(kernelTunnel, userspaceTunnel, appDataRepository, applicationScope, ioDispatcher)
|
return TunnelManager(kernelTunnel, userspaceTunnel, appDataRepository, applicationScope, ioDispatcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideNetworkMonitor(@ApplicationContext context: Context, settingsRepository: AppSettingRepository): NetworkMonitor {
|
|
||||||
return AndroidNetworkMonitor(context) { runBlocking { settingsRepository.get().isWifiNameByShellEnabled } }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun provideServiceManager(
|
fun provideServiceManager(
|
||||||
@ApplicationContext context: Context,
|
@ApplicationContext context: Context,
|
||||||
@IoDispatcher ioDispatcher: CoroutineDispatcher,
|
@IoDispatcher ioDispatcher: CoroutineDispatcher,
|
||||||
@MainDispatcher mainCoroutineDispatcher: CoroutineDispatcher,
|
|
||||||
@ApplicationScope applicationScope: CoroutineScope,
|
|
||||||
appDataRepository: AppDataRepository,
|
appDataRepository: AppDataRepository,
|
||||||
): ServiceManager {
|
): ServiceManager {
|
||||||
return ServiceManager(context, ioDispatcher, applicationScope, mainCoroutineDispatcher, appDataRepository)
|
return ServiceManager(context, ioDispatcher, appDataRepository)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.domain.entity
|
package com.zaneschepke.wireguardautotunnel.domain.entity
|
||||||
|
|
||||||
import com.wireguard.android.backend.Tunnel
|
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.defaultName
|
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.extractNameAndNumber
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.hasNumberInParentheses
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString
|
import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.amnezia.awg.backend.Tunnel
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
data class TunnelConf(
|
data class TunnelConf(
|
||||||
@@ -29,67 +29,10 @@ data class TunnelConf(
|
|||||||
val pingCooldown: Long? = null,
|
val pingCooldown: Long? = null,
|
||||||
val pingIp: String? = null,
|
val pingIp: String? = null,
|
||||||
val isEthernetTunnel: Boolean = false,
|
val isEthernetTunnel: Boolean = false,
|
||||||
val isIpv4Preferred: Boolean = true,
|
val isIpv4Preferred: Boolean = false,
|
||||||
@Transient
|
) : Tunnel, com.wireguard.android.backend.Tunnel {
|
||||||
private var stateChangeCallback: ((Any) -> Unit)? = null,
|
|
||||||
) : Tunnel, org.amnezia.awg.backend.Tunnel {
|
|
||||||
|
|
||||||
fun setStateChangeCallback(callback: (Any) -> Unit) {
|
val state = MutableStateFlow(TunnelState())
|
||||||
stateChangeCallback = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (other !is TunnelConf) return false
|
|
||||||
return id == other.id && tunName == other.tunName && wgQuick == other.wgQuick && amQuick == other.amQuick &&
|
|
||||||
isPrimaryTunnel == other.isPrimaryTunnel && isMobileDataTunnel == other.isMobileDataTunnel &&
|
|
||||||
isEthernetTunnel == other.isEthernetTunnel && isPingEnabled == other.isPingEnabled && pingIp == other.pingIp &&
|
|
||||||
pingCooldown == other.pingCooldown && pingInterval == other.pingInterval && tunnelNetworks == other.tunnelNetworks &&
|
|
||||||
isIpv4Preferred == other.isIpv4Preferred
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = id
|
|
||||||
result = 31 * result + tunName.hashCode()
|
|
||||||
result = 31 * result + wgQuick.hashCode()
|
|
||||||
result = 31 * result + amQuick.hashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
fun copyWithCallback(
|
|
||||||
id: Int = this.id,
|
|
||||||
tunName: String = this.tunName,
|
|
||||||
wgQuick: String = this.wgQuick,
|
|
||||||
tunnelNetworks: List<String> = this.tunnelNetworks,
|
|
||||||
isMobileDataTunnel: Boolean = this.isMobileDataTunnel,
|
|
||||||
isPrimaryTunnel: Boolean = this.isPrimaryTunnel,
|
|
||||||
amQuick: String = this.amQuick,
|
|
||||||
isActive: Boolean = this.isActive,
|
|
||||||
isPingEnabled: Boolean = this.isPingEnabled,
|
|
||||||
pingInterval: Long? = this.pingInterval,
|
|
||||||
pingCooldown: Long? = this.pingCooldown,
|
|
||||||
pingIp: String? = this.pingIp,
|
|
||||||
isEthernetTunnel: Boolean = this.isEthernetTunnel,
|
|
||||||
isIpv4Preferred: Boolean = this.isIpv4Preferred,
|
|
||||||
): TunnelConf {
|
|
||||||
return TunnelConf(
|
|
||||||
id, tunName, wgQuick, tunnelNetworks, isMobileDataTunnel, isPrimaryTunnel,
|
|
||||||
amQuick, isActive, isPingEnabled, pingInterval, pingCooldown, pingIp,
|
|
||||||
isEthernetTunnel, isIpv4Preferred,
|
|
||||||
).apply {
|
|
||||||
stateChangeCallback = this@TunnelConf.stateChangeCallback
|
|
||||||
// tunnelStatsCallback = this@TunnelConf.tunnelStatsCallback
|
|
||||||
// bounceTunnelCallback = this@TunnelConf.bounceTunnelCallback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fun onUpdateStatistics() {
|
|
||||||
// tunnelStatsCallback?.invoke()
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// fun bounceTunnel(tunnelConf: TunnelConf, reason: TunnelStatus.StopReason) {
|
|
||||||
// bounceTunnelCallback?.invoke(tunnelConf, reason)
|
|
||||||
// }
|
|
||||||
|
|
||||||
fun toAmConfig(): org.amnezia.awg.config.Config {
|
fun toAmConfig(): org.amnezia.awg.config.Config {
|
||||||
return configFromAmQuick(amQuick.ifBlank { wgQuick })
|
return configFromAmQuick(amQuick.ifBlank { wgQuick })
|
||||||
@@ -99,50 +42,49 @@ data class TunnelConf(
|
|||||||
return configFromWgQuick(wgQuick)
|
return configFromWgQuick(wgQuick)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getName(): String = tunName
|
override fun getName(): String {
|
||||||
|
return tunName
|
||||||
|
}
|
||||||
|
|
||||||
override fun isIpv4ResolutionPreferred(): Boolean = isIpv4Preferred
|
override fun isIpv4ResolutionPreferred(): Boolean {
|
||||||
|
return isIpv4Preferred
|
||||||
override fun onStateChange(newState: org.amnezia.awg.backend.Tunnel.State) {
|
|
||||||
stateChangeCallback?.invoke(newState)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStateChange(newState: Tunnel.State) {
|
override fun onStateChange(newState: Tunnel.State) {
|
||||||
stateChangeCallback?.invoke(newState)
|
state.update {
|
||||||
|
it.copy(state = newState.asTunnelState())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isTunnelConfigChanged(updatedConf: TunnelConf): Boolean {
|
override fun onStateChange(newState: com.wireguard.android.backend.Tunnel.State) {
|
||||||
return updatedConf.wgQuick != wgQuick || updatedConf.amQuick != amQuick || updatedConf.name != name
|
state.update {
|
||||||
|
it.copy(state = newState.asTunnelState())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun generateUniqueName(tunnelNames: List<String>): String {
|
fun isQuickConfigChanged(updatedConf: TunnelConf): Boolean {
|
||||||
var tunnelName = this.tunName
|
return updatedConf.wgQuick != wgQuick ||
|
||||||
var num = 1
|
updatedConf.amQuick != amQuick
|
||||||
while (tunnelNames.any { it == tunnelName }) {
|
|
||||||
tunnelName = if (!tunnelName.hasNumberInParentheses()) {
|
|
||||||
"$name($num)"
|
|
||||||
} else {
|
|
||||||
val pair = tunnelName.extractNameAndNumber()
|
|
||||||
"${pair?.first}($num)"
|
|
||||||
}
|
|
||||||
num++
|
|
||||||
}
|
|
||||||
return tunnelName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun isTunnelPingable(context: CoroutineContext): Boolean {
|
fun isPingConfigMatching(updatedConf: TunnelConf): Boolean {
|
||||||
|
return updatedConf.isPingEnabled == isPingEnabled &&
|
||||||
|
pingIp == updatedConf.pingIp &&
|
||||||
|
updatedConf.pingCooldown == pingCooldown &&
|
||||||
|
updatedConf.pingInterval == pingInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun pingTunnel(context: CoroutineContext): List<Boolean> {
|
||||||
return withContext(context) {
|
return withContext(context) {
|
||||||
val config = toWgConfig()
|
val config = toWgConfig()
|
||||||
if (pingIp != null) {
|
if (pingIp != null) {
|
||||||
return@withContext InetAddress.getByName(pingIp)
|
Timber.i("Pinging custom ip")
|
||||||
.isReachable(Constants.PING_TIMEOUT.toInt()).also {
|
listOf(InetAddress.getByName(pingIp).isReachable(Constants.PING_TIMEOUT.toInt()))
|
||||||
Timber.i("Ping reachable $pingIp: $it")
|
} else {
|
||||||
}
|
Timber.i("Pinging all peers")
|
||||||
}
|
|
||||||
config.peers.map { peer ->
|
config.peers.map { peer ->
|
||||||
peer.isReachable()
|
peer.isReachable(isIpv4Preferred)
|
||||||
}.all { true }.also {
|
}
|
||||||
Timber.i("Ping of all peers reachable: $it")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,22 +92,22 @@ data class TunnelConf(
|
|||||||
companion object {
|
companion object {
|
||||||
fun configFromWgQuick(wgQuick: String): Config {
|
fun configFromWgQuick(wgQuick: String): Config {
|
||||||
val inputStream: InputStream = wgQuick.byteInputStream()
|
val inputStream: InputStream = wgQuick.byteInputStream()
|
||||||
return inputStream.bufferedReader(StandardCharsets.UTF_8).use {
|
return inputStream.bufferedReader(Charsets.UTF_8).use {
|
||||||
Config.parse(it)
|
Config.parse(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun configFromAmQuick(amQuick: String): org.amnezia.awg.config.Config {
|
fun configFromAmQuick(amQuick: String): org.amnezia.awg.config.Config {
|
||||||
val inputStream: InputStream = amQuick.byteInputStream()
|
val inputStream: InputStream = amQuick.byteInputStream()
|
||||||
return inputStream.bufferedReader(StandardCharsets.UTF_8).use {
|
return inputStream.bufferedReader(Charsets.UTF_8).use {
|
||||||
org.amnezia.awg.config.Config.parse(it)
|
org.amnezia.awg.config.Config.parse(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun tunnelConfigFromAmConfig(config: org.amnezia.awg.config.Config, name: String? = null): TunnelConf {
|
fun tunnelConfigFromAmConfig(config: org.amnezia.awg.config.Config, name: String): TunnelConf {
|
||||||
val amQuick = config.toAwgQuickString(true)
|
val amQuick = config.toAwgQuickString(true)
|
||||||
val wgQuick = config.toWgQuickString()
|
val wgQuick = config.toWgQuickString()
|
||||||
return TunnelConf(tunName = name ?: config.defaultName(), wgQuick = wgQuick, amQuick = amQuick)
|
return TunnelConf(tunName = name, wgQuick = wgQuick, amQuick = amQuick)
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val IPV6_ALL_NETWORKS = "::/0"
|
private const val IPV6_ALL_NETWORKS = "::/0"
|
||||||
|
|||||||
@@ -1,24 +1,7 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.domain.enums
|
package com.zaneschepke.wireguardautotunnel.domain.enums
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
sealed class BackendError() {
|
||||||
|
|
||||||
sealed class BackendError : Exception() {
|
|
||||||
data object DNS : BackendError()
|
data object DNS : BackendError()
|
||||||
data object Unauthorized : BackendError()
|
data object Unauthorized : BackendError()
|
||||||
data object Config : BackendError()
|
data object Config : BackendError()
|
||||||
data object KernelModuleName : BackendError()
|
|
||||||
data object InvalidConfig : BackendError()
|
|
||||||
data object NotAuthorized : BackendError()
|
|
||||||
data object ServiceNotRunning : BackendError()
|
|
||||||
data object Unknown : BackendError()
|
|
||||||
|
|
||||||
fun toStringRes() = when (this) {
|
|
||||||
Config -> R.string.config_error
|
|
||||||
DNS -> R.string.dns_resolve_error
|
|
||||||
InvalidConfig -> R.string.invalid_config_error
|
|
||||||
KernelModuleName -> R.string.kernel_name_error
|
|
||||||
NotAuthorized, Unauthorized -> R.string.auth_error
|
|
||||||
ServiceNotRunning -> R.string.service_running_error
|
|
||||||
Unknown -> R.string.unknown_error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,15 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.domain.enums
|
package com.zaneschepke.wireguardautotunnel.domain.enums
|
||||||
|
|
||||||
sealed class TunnelStatus {
|
enum class TunnelStatus {
|
||||||
data class Error(val error: BackendError) : TunnelStatus()
|
UP,
|
||||||
data object Up : TunnelStatus()
|
DOWN,
|
||||||
data object Down : TunnelStatus()
|
;
|
||||||
data class Stopping(val reason: StopReason) : TunnelStatus()
|
|
||||||
data object Starting : TunnelStatus()
|
|
||||||
|
|
||||||
enum class StopReason {
|
|
||||||
USER,
|
|
||||||
PING,
|
|
||||||
CONFIG_CHANGED,
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isDown(): Boolean {
|
fun isDown(): Boolean {
|
||||||
return this == Down
|
return this == DOWN
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isUp(): Boolean {
|
fun isUp(): Boolean {
|
||||||
return this == Up
|
return this == UP
|
||||||
}
|
|
||||||
|
|
||||||
fun isUpOrStarting(): Boolean {
|
|
||||||
return this == Up || this == Starting
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isDownOrStopping(): Boolean {
|
|
||||||
return this == Down || this is Stopping
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-2
@@ -11,8 +11,6 @@ interface TunnelRepository {
|
|||||||
|
|
||||||
suspend fun save(tunnelConf: TunnelConf)
|
suspend fun save(tunnelConf: TunnelConf)
|
||||||
|
|
||||||
suspend fun saveAll(tunnelConfList: List<TunnelConf>)
|
|
||||||
|
|
||||||
suspend fun updatePrimaryTunnel(tunnelConf: TunnelConf?)
|
suspend fun updatePrimaryTunnel(tunnelConf: TunnelConf?)
|
||||||
|
|
||||||
suspend fun updateMobileDataTunnel(tunnelConf: TunnelConf?)
|
suspend fun updateMobileDataTunnel(tunnelConf: TunnelConf?)
|
||||||
|
|||||||
+14
-17
@@ -1,16 +1,13 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.domain.state
|
package com.zaneschepke.wireguardautotunnel.domain.state
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.allDown
|
import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.hasActive
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.isUp
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
|
||||||
|
|
||||||
data class AutoTunnelState(
|
data class AutoTunnelState(
|
||||||
val activeTunnels: Map<TunnelConf, TunnelState> = emptyMap(),
|
val activeTunnels: List<TunnelConf> = emptyList(),
|
||||||
val networkState: NetworkState = NetworkState(),
|
val networkState: NetworkState = NetworkState(),
|
||||||
val settings: AppSettings = AppSettings(),
|
val settings: AppSettings = AppSettings(),
|
||||||
val tunnels: List<TunnelConf> = emptyList(),
|
val tunnels: List<TunnelConf> = emptyList(),
|
||||||
@@ -23,12 +20,12 @@ data class AutoTunnelState(
|
|||||||
private fun isMobileTunnelDataChangeNeeded(): Boolean {
|
private fun isMobileTunnelDataChangeNeeded(): Boolean {
|
||||||
val preferredTunnel = preferredMobileDataTunnel()
|
val preferredTunnel = preferredMobileDataTunnel()
|
||||||
return preferredTunnel != null &&
|
return preferredTunnel != null &&
|
||||||
activeTunnels.isNotEmpty() && !activeTunnels.isUp(preferredTunnel)
|
activeTunnels.isNotEmpty() && !activeTunnels.any { it.id == preferredTunnel.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isEthernetTunnelChangeNeeded(): Boolean {
|
private fun isEthernetTunnelChangeNeeded(): Boolean {
|
||||||
val preferredTunnel = preferredEthernetTunnel()
|
val preferredTunnel = preferredEthernetTunnel()
|
||||||
return preferredTunnel != null && activeTunnels.isNotEmpty() && !activeTunnels.isUp(preferredTunnel)
|
return preferredTunnel != null && activeTunnels.isNotEmpty() && !activeTunnels.any { it.id == preferredTunnel.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun preferredMobileDataTunnel(): TunnelConf? {
|
private fun preferredMobileDataTunnel(): TunnelConf? {
|
||||||
@@ -48,11 +45,11 @@ data class AutoTunnelState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun startOnEthernet(): Boolean {
|
private fun startOnEthernet(): Boolean {
|
||||||
return networkState.isEthernetConnected && settings.isTunnelOnEthernetEnabled && activeTunnels.allDown()
|
return networkState.isEthernetConnected && settings.isTunnelOnEthernetEnabled && activeTunnels.isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopOnEthernet(): Boolean {
|
private fun stopOnEthernet(): Boolean {
|
||||||
return networkState.isEthernetConnected && !settings.isTunnelOnEthernetEnabled && activeTunnels.hasActive()
|
return networkState.isEthernetConnected && !settings.isTunnelOnEthernetEnabled && activeTunnels.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO test removed kill switch state check
|
// TODO test removed kill switch state check
|
||||||
@@ -65,16 +62,16 @@ data class AutoTunnelState(
|
|||||||
return settings.isVpnKillSwitchEnabled && (!settings.isDisableKillSwitchOnTrustedEnabled || !isCurrentSSIDTrusted())
|
return settings.isVpnKillSwitchEnabled && (!settings.isDisableKillSwitchOnTrustedEnabled || !isCurrentSSIDTrusted())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isNoConnectivity(): Boolean {
|
fun isNoConnectivity(): Boolean {
|
||||||
return !networkState.isEthernetConnected && !networkState.isWifiConnected && !networkState.isMobileDataConnected
|
return !networkState.isEthernetConnected && !networkState.isWifiConnected && !networkState.isMobileDataConnected
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopOnMobileData(): Boolean {
|
private fun stopOnMobileData(): Boolean {
|
||||||
return isMobileDataActive() && !settings.isTunnelOnMobileDataEnabled && activeTunnels.hasActive()
|
return isMobileDataActive() && !settings.isTunnelOnMobileDataEnabled && activeTunnels.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startOnMobileData(): Boolean {
|
private fun startOnMobileData(): Boolean {
|
||||||
return isMobileDataActive() && settings.isTunnelOnMobileDataEnabled && activeTunnels.allDown()
|
return isMobileDataActive() && settings.isTunnelOnMobileDataEnabled && activeTunnels.isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun changeOnMobileData(): Boolean {
|
private fun changeOnMobileData(): Boolean {
|
||||||
@@ -86,24 +83,24 @@ data class AutoTunnelState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun stopOnWifi(): Boolean {
|
private fun stopOnWifi(): Boolean {
|
||||||
return isWifiActive() && !settings.isTunnelOnWifiEnabled && activeTunnels.hasActive()
|
return isWifiActive() && !settings.isTunnelOnWifiEnabled && activeTunnels.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopOnTrustedWifi(): Boolean {
|
private fun stopOnTrustedWifi(): Boolean {
|
||||||
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.hasActive() && isCurrentSSIDTrusted()
|
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.isNotEmpty() && isCurrentSSIDTrusted()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startOnUntrustedWifi(): Boolean {
|
private fun startOnUntrustedWifi(): Boolean {
|
||||||
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.allDown() && !isCurrentSSIDTrusted()
|
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.isEmpty() && !isCurrentSSIDTrusted()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun changeOnUntrustedWifi(): Boolean {
|
private fun changeOnUntrustedWifi(): Boolean {
|
||||||
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.hasActive() && !isCurrentSSIDTrusted() && !isWifiTunnelPreferred()
|
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.isNotEmpty() && !isCurrentSSIDTrusted() && !isWifiTunnelPreferred()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isWifiTunnelPreferred(): Boolean {
|
private fun isWifiTunnelPreferred(): Boolean {
|
||||||
val preferred = preferredWifiTunnel()
|
val preferred = preferredWifiTunnel()
|
||||||
return preferred?.let { activeTunnels.isUp(it) } ?: true
|
return activeTunnels.any { it.id == preferred?.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun asAutoTunnelEvent(): AutoTunnelEvent {
|
fun asAutoTunnelEvent(): AutoTunnelEvent {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
|||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||||
|
|
||||||
data class TunnelState(
|
data class TunnelState(
|
||||||
val status: TunnelStatus = TunnelStatus.Down,
|
val state: TunnelStatus = TunnelStatus.DOWN,
|
||||||
val backendState: BackendState = BackendState.INACTIVE,
|
val backendState: BackendState = BackendState.INACTIVE,
|
||||||
val statistics: TunnelStatistics? = null,
|
val statistics: TunnelStatistics? = null,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -52,11 +52,7 @@ sealed class Route {
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class SplitTunnel(
|
data class SplitTunnel(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
) : Route() {
|
) : Route()
|
||||||
companion object {
|
|
||||||
const val KEY_ID = "id"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class TunnelAutoTunnel(
|
data class TunnelAutoTunnel(
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.common.button
|
package com.zaneschepke.wireguardautotunnel.ui.common
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.common
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun <T> DropdownSelector(
|
|
||||||
currentValue: T,
|
|
||||||
options: List<T>,
|
|
||||||
onValueSelected: (T) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
label: @Composable (() -> Unit)? = null,
|
|
||||||
isExpanded: Boolean = false,
|
|
||||||
onDismiss: () -> Unit = {},
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
if (label != null) label()
|
|
||||||
Text(
|
|
||||||
text = currentValue.toString(),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
)
|
|
||||||
Icon(Icons.Default.ArrowDropDown, contentDescription = stringResource(R.string.dropdown))
|
|
||||||
}
|
|
||||||
DropdownMenu(
|
|
||||||
modifier = modifier.height(250.dp),
|
|
||||||
scrollState = rememberScrollState(),
|
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
|
||||||
expanded = isExpanded,
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
) {
|
|
||||||
options.forEach { option ->
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(text = option.toString()) },
|
|
||||||
onClick = {
|
|
||||||
onValueSelected(option)
|
|
||||||
onDismiss() // Close dropdown after selection
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common
|
||||||
|
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
|
|
||||||
|
class NestedScrollListener(val onUp: () -> Unit, val onDown: () -> Unit) : NestedScrollConnection {
|
||||||
|
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||||
|
if (available.y < -1) onDown()
|
||||||
|
if (available.y > 1) onUp()
|
||||||
|
return Offset.Zero
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.Clear
|
||||||
|
import androidx.compose.material.icons.rounded.Search
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SearchBar(onQuery: (queryString: String) -> Unit) {
|
||||||
|
// Immediately update and keep track of query from text field changes.
|
||||||
|
var query: String by rememberSaveable { mutableStateOf("") }
|
||||||
|
var showClearIcon by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (query.isEmpty()) {
|
||||||
|
showClearIcon = false
|
||||||
|
} else if (query.isNotEmpty()) {
|
||||||
|
showClearIcon = true
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
value = query,
|
||||||
|
onValueChange = { onQueryChanged ->
|
||||||
|
// If user makes changes to text, immediately updated it.
|
||||||
|
query = onQueryChanged
|
||||||
|
onQuery(onQueryChanged)
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
val icon = Icons.Rounded.Search
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
|
contentDescription = icon.name,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
if (showClearIcon) {
|
||||||
|
IconButton(onClick = { query = "" }) {
|
||||||
|
val icon = Icons.Rounded.Clear
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
|
contentDescription = icon.name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
maxLines = 1,
|
||||||
|
colors =
|
||||||
|
TextFieldDefaults.colors(
|
||||||
|
focusedContainerColor = Color.Transparent,
|
||||||
|
unfocusedContainerColor = Color.Transparent,
|
||||||
|
disabledContainerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
placeholder = { Text(text = stringResource(R.string.hint_search_packages)) },
|
||||||
|
textStyle = MaterialTheme.typography.bodySmall,
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape),
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.common.label
|
package com.zaneschepke.wireguardautotunnel.ui.common
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
-37
@@ -1,37 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.common.animation
|
|
||||||
|
|
||||||
import androidx.compose.animation.core.LinearEasing
|
|
||||||
import androidx.compose.animation.core.animateFloat
|
|
||||||
import androidx.compose.animation.core.infiniteRepeatable
|
|
||||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ShimmerEffect(modifier: Modifier = Modifier): Brush {
|
|
||||||
val shimmerColors = listOf(
|
|
||||||
Color.LightGray.copy(alpha = 0.9f),
|
|
||||||
Color.LightGray.copy(alpha = 0.3f),
|
|
||||||
Color.LightGray.copy(alpha = 0.9f),
|
|
||||||
)
|
|
||||||
|
|
||||||
val transition = rememberInfiniteTransition()
|
|
||||||
val translateAnim by transition.animateFloat(
|
|
||||||
initialValue = 0f,
|
|
||||||
targetValue = 1000f,
|
|
||||||
animationSpec = infiniteRepeatable(
|
|
||||||
animation = tween(durationMillis = 1200, easing = LinearEasing),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return Brush.linearGradient(
|
|
||||||
colors = shimmerColors,
|
|
||||||
start = Offset(0f, 0f),
|
|
||||||
end = Offset(translateAnim, translateAnim),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+7
-5
@@ -23,6 +23,8 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||||
|
|
||||||
@androidx.compose.runtime.Composable
|
@androidx.compose.runtime.Composable
|
||||||
fun IconSurfaceButton(title: String, onClick: () -> Unit, selected: Boolean, leadingIcon: ImageVector? = null, description: String? = null) {
|
fun IconSurfaceButton(title: String, onClick: () -> Unit, selected: Boolean, leadingIcon: ImageVector? = null, description: String? = null) {
|
||||||
@@ -51,22 +53,22 @@ fun IconSurfaceButton(title: String, onClick: () -> Unit, selected: Boolean, lea
|
|||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(horizontal = 8.dp, vertical = 10.dp)
|
.padding(horizontal = 8.dp.scaledWidth(), vertical = 10.dp.scaledHeight())
|
||||||
.padding(end = 16.dp).padding(start = 8.dp)
|
.padding(end = 16.dp.scaledWidth()).padding(start = 8.dp.scaledWidth())
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.Companion.CenterVertically,
|
verticalAlignment = Alignment.Companion.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp.scaledWidth()),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(
|
horizontalArrangement = Arrangement.spacedBy(
|
||||||
16.dp,
|
16.dp.scaledWidth(),
|
||||||
),
|
),
|
||||||
verticalAlignment = Alignment.Companion.CenterVertically,
|
verticalAlignment = Alignment.Companion.CenterVertically,
|
||||||
modifier = Modifier.padding(vertical = if (description == null) 10.dp else 0.dp),
|
modifier = Modifier.padding(vertical = if (description == null) 10.dp.scaledHeight() else 0.dp),
|
||||||
) {
|
) {
|
||||||
leadingIcon?.let {
|
leadingIcon?.let {
|
||||||
Icon(
|
Icon(
|
||||||
|
|||||||
+2
-1
@@ -7,13 +7,14 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ScaledSwitch(checked: Boolean, onClick: (checked: Boolean) -> Unit, enabled: Boolean = true, modifier: Modifier = Modifier) {
|
fun ScaledSwitch(checked: Boolean, onClick: (checked: Boolean) -> Unit, enabled: Boolean = true, modifier: Modifier = Modifier) {
|
||||||
Switch(
|
Switch(
|
||||||
checked,
|
checked,
|
||||||
{ onClick(it) },
|
{ onClick(it) },
|
||||||
modifier.scale((52.dp / 52.dp)),
|
modifier.scale((52.dp.scaledHeight() / 52.dp)),
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
colors = SwitchDefaults.colors().copy(
|
colors = SwitchDefaults.colors().copy(
|
||||||
checkedThumbColor = MaterialTheme.colorScheme.background,
|
checkedThumbColor = MaterialTheme.colorScheme.background,
|
||||||
|
|||||||
+6
-6
@@ -19,9 +19,10 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SelectionItemButton(
|
fun SelectionItemButton(
|
||||||
@@ -30,21 +31,20 @@ fun SelectionItemButton(
|
|||||||
trailing: (@Composable () -> Unit)? = null,
|
trailing: (@Composable () -> Unit)? = null,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
ripple: Boolean = true,
|
ripple: Boolean = true,
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier =
|
modifier =
|
||||||
modifier
|
Modifier
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(8.dp))
|
||||||
.clickable(
|
.clickable(
|
||||||
indication = if (ripple) ripple() else null,
|
indication = if (ripple) ripple() else null,
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
onClick = { onClick() },
|
onClick = { onClick() },
|
||||||
)
|
)
|
||||||
.height(56.dp),
|
.height(56.dp.scaledHeight()),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@@ -52,7 +52,7 @@ fun SelectionItemButton(
|
|||||||
horizontalArrangement = Arrangement.Start,
|
horizontalArrangement = Arrangement.Start,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(end = 10.dp),
|
.padding(end = 10.dp.scaledWidth()),
|
||||||
) {
|
) {
|
||||||
leading?.let {
|
leading?.let {
|
||||||
it()
|
it()
|
||||||
|
|||||||
-20
@@ -1,20 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.common.button.surface
|
|
||||||
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SelectionItemLabel(
|
|
||||||
textResId: Int,
|
|
||||||
style: androidx.compose.ui.text.TextStyle = MaterialTheme.typography.bodyMedium,
|
|
||||||
isDescription: Boolean = false,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(textResId),
|
|
||||||
style = style.copy(
|
|
||||||
color = if (isDescription) MaterialTheme.colorScheme.outline else MaterialTheme.colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+7
-5
@@ -19,6 +19,8 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SurfaceSelectionGroupButton(items: List<SelectionItem>) {
|
fun SurfaceSelectionGroupButton(items: List<SelectionItem>) {
|
||||||
@@ -36,12 +38,12 @@ fun SurfaceSelectionGroupButton(items: List<SelectionItem>) {
|
|||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = 16.dp)
|
.padding(start = 16.dp.scaledWidth())
|
||||||
.weight(4f, false)
|
.weight(4f, false)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
@@ -58,8 +60,8 @@ fun SurfaceSelectionGroupButton(items: List<SelectionItem>) {
|
|||||||
verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
|
verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(start = if (item.leadingIcon != null) 16.dp else 0.dp)
|
.padding(start = if (item.leadingIcon != null) 16.dp.scaledWidth() else 0.dp)
|
||||||
.padding(vertical = if (item.description == null) 16.dp else 6.dp),
|
.padding(vertical = if (item.description == null) 16.dp.scaledHeight() else 6.dp.scaledHeight()),
|
||||||
) {
|
) {
|
||||||
item.title()
|
item.title()
|
||||||
item.description?.let {
|
item.description?.let {
|
||||||
@@ -71,7 +73,7 @@ fun SurfaceSelectionGroupButton(items: List<SelectionItem>) {
|
|||||||
Box(
|
Box(
|
||||||
contentAlignment = Alignment.CenterEnd,
|
contentAlignment = Alignment.CenterEnd,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(end = 24.dp, start = 16.dp)
|
.padding(end = 24.dp.scaledWidth(), start = 16.dp.scaledWidth())
|
||||||
.weight(1f),
|
.weight(1f),
|
||||||
) {
|
) {
|
||||||
it()
|
it()
|
||||||
|
|||||||
+2
-2
@@ -35,9 +35,9 @@ fun ConfigurationTextBox(
|
|||||||
singleLine = true,
|
singleLine = true,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
onValueChange = { onValueChange(it) },
|
onValueChange = { onValueChange(it) },
|
||||||
label = { Text(label, color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.bodyMedium) },
|
label = { Text(label, color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.labelMedium) },
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
placeholder = { Text(hint, color = MaterialTheme.colorScheme.outline, style = MaterialTheme.typography.bodyMedium) },
|
placeholder = { Text(hint, color = MaterialTheme.colorScheme.outline, style = MaterialTheme.typography.labelLarge) },
|
||||||
keyboardOptions = keyboardOptions,
|
keyboardOptions = keyboardOptions,
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
trailingIcon = trailing,
|
trailingIcon = trailing,
|
||||||
|
|||||||
+2
-1
@@ -24,6 +24,7 @@ import androidx.compose.ui.text.input.ImeAction
|
|||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField
|
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SubmitConfigurationTextBox(
|
fun SubmitConfigurationTextBox(
|
||||||
@@ -58,7 +59,7 @@ fun SubmitConfigurationTextBox(
|
|||||||
.padding(
|
.padding(
|
||||||
top = 5.dp,
|
top = 5.dp,
|
||||||
bottom = 10.dp,
|
bottom = 10.dp,
|
||||||
).fillMaxWidth().padding(end = 16.dp),
|
).fillMaxWidth().padding(end = 16.dp.scaledWidth()),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
keyboardOptions = keyboardOptions,
|
keyboardOptions = keyboardOptions,
|
||||||
keyboardActions = KeyboardActions(
|
keyboardActions = KeyboardActions(
|
||||||
|
|||||||
+5
-18
@@ -1,14 +1,10 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.common.dialog
|
package com.zaneschepke.wireguardautotunnel.ui.common.dialog
|
||||||
|
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.window.DialogProperties
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -19,17 +15,14 @@ fun InfoDialog(
|
|||||||
body: @Composable () -> Unit,
|
body: @Composable () -> Unit,
|
||||||
confirmText: @Composable () -> Unit,
|
confirmText: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
MaterialTheme(
|
|
||||||
colorScheme = MaterialTheme.colorScheme.copy(),
|
|
||||||
) {
|
|
||||||
Surface(
|
|
||||||
color = MaterialTheme.colorScheme.surface,
|
|
||||||
tonalElevation = 0.dp,
|
|
||||||
) {
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { onDismiss() },
|
onDismissRequest = { onDismiss() },
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = { onAttest() }) {
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
onAttest()
|
||||||
|
},
|
||||||
|
) {
|
||||||
confirmText()
|
confirmText()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -38,13 +31,7 @@ fun InfoDialog(
|
|||||||
Text(text = stringResource(R.string.cancel))
|
Text(text = stringResource(R.string.cancel))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
|
||||||
title = { title() },
|
title = { title() },
|
||||||
text = { body() },
|
text = { body() },
|
||||||
properties = DialogProperties(
|
|
||||||
usePlatformDefaultWidth = true,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ 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.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GroupLabel(title: String) {
|
fun GroupLabel(title: String) {
|
||||||
@@ -17,7 +16,6 @@ fun GroupLabel(title: String) {
|
|||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-8
@@ -1,4 +1,4 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support.components
|
package com.zaneschepke.wireguardautotunnel.ui.common.label
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -13,7 +13,6 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VersionLabel() {
|
fun VersionLabel() {
|
||||||
@@ -22,13 +21,8 @@ fun VersionLabel() {
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Start,
|
horizontalArrangement = Arrangement.Start,
|
||||||
) {
|
) {
|
||||||
val versionText = if (BuildConfig.BUILD_TYPE == Constants.RELEASE) {
|
|
||||||
BuildConfig.VERSION_NAME
|
|
||||||
} else {
|
|
||||||
"${BuildConfig.VERSION_NAME}-${BuildConfig.BUILD_TYPE}"
|
|
||||||
}
|
|
||||||
Text(
|
Text(
|
||||||
"${stringResource(R.string.version)}: $versionText",
|
"${stringResource(R.string.version)}: ${BuildConfig.VERSION_NAME}",
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = MaterialTheme.colorScheme.outline,
|
color = MaterialTheme.colorScheme.outline,
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
-93
@@ -1,93 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.common.navigation
|
|
||||||
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
|
||||||
import androidx.compose.animation.core.Spring
|
|
||||||
import androidx.compose.animation.core.spring
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.offset
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.material3.Badge
|
|
||||||
import androidx.compose.material3.BadgedBox
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun BottomBarTabs(tabs: List<BottomNavItem>, selectedTabIndex: Int, isChildRoute: Boolean, onTabSelected: (BottomNavItem) -> Unit) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(64.dp)
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.padding(top = 12.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
tabs.forEachIndexed { index, tab ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.fillMaxHeight()
|
|
||||||
.background(Color.Transparent)
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectTapGestures {
|
|
||||||
if (index == selectedTabIndex && !isChildRoute) return@detectTapGestures
|
|
||||||
tab.onClick.invoke()
|
|
||||||
onTabSelected(tab)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
) {
|
|
||||||
val animatedColor by animateColorAsState(
|
|
||||||
targetValue = MaterialTheme.colorScheme.primary,
|
|
||||||
animationSpec = spring(stiffness = Spring.StiffnessLow),
|
|
||||||
label = "animatedColor",
|
|
||||||
)
|
|
||||||
val color = if (selectedTabIndex == index) animatedColor else MaterialTheme.colorScheme.onSurface
|
|
||||||
|
|
||||||
if (tab.active) {
|
|
||||||
BadgedBox(
|
|
||||||
badge = {
|
|
||||||
Badge(
|
|
||||||
modifier = Modifier
|
|
||||||
.offset(x = 8.dp, y = ((-8).dp))
|
|
||||||
.size(6.dp),
|
|
||||||
containerColor = SilverTree,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = tab.icon,
|
|
||||||
contentDescription = tab.name,
|
|
||||||
tint = color,
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Icon(
|
|
||||||
imageVector = tab.icon,
|
|
||||||
contentDescription = tab.name,
|
|
||||||
tint = color,
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common.navigation
|
||||||
|
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.NavigationBar
|
||||||
|
import androidx.compose.material3.NavigationBarItem
|
||||||
|
import androidx.compose.material3.NavigationBarItemDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||||
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) {
|
||||||
|
var showBottomBar by rememberSaveable { mutableStateOf(true) }
|
||||||
|
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||||
|
|
||||||
|
showBottomBar = bottomNavItems.any {
|
||||||
|
navBackStackEntry?.isCurrentRoute(it.route::class) == true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showBottomBar) {
|
||||||
|
NavigationBar(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
) {
|
||||||
|
bottomNavItems.forEachIndexed { index, item ->
|
||||||
|
val selected = navBackStackEntry.isCurrentRoute(item.route::class)
|
||||||
|
NavigationBarItem(
|
||||||
|
selected = selected,
|
||||||
|
onClick = {
|
||||||
|
if (selected) return@NavigationBarItem
|
||||||
|
navController.navigate(item.route) {
|
||||||
|
// Pop up to the start destination of the graph to
|
||||||
|
// avoid building up a large stack of destinations
|
||||||
|
// on the back stack as users select items
|
||||||
|
popUpTo(navController.graph.findStartDestination().id) {
|
||||||
|
saveState = true
|
||||||
|
}
|
||||||
|
// Avoid multiple copies of the same destination when
|
||||||
|
// reselecting the same item
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = item.name,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = item.icon,
|
||||||
|
contentDescription = "${item.name} Icon",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = NavigationBarItemDefaults.colors().copy(
|
||||||
|
selectedIndicatorColor = Color.Transparent,
|
||||||
|
selectedIconColor = MaterialTheme.colorScheme.primary,
|
||||||
|
selectedTextColor = MaterialTheme.colorScheme.primary,
|
||||||
|
unselectedTextColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.55f),
|
||||||
|
unselectedIconColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.55f),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-2
@@ -7,6 +7,4 @@ data class BottomNavItem(
|
|||||||
val name: String,
|
val name: String,
|
||||||
val route: Route,
|
val route: Route,
|
||||||
val icon: ImageVector,
|
val icon: ImageVector,
|
||||||
val onClick: () -> Unit,
|
|
||||||
val active: Boolean = false,
|
|
||||||
)
|
)
|
||||||
|
|||||||
-133
@@ -1,133 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.common.navigation
|
|
||||||
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
|
||||||
import androidx.compose.animation.core.Spring
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
|
||||||
import androidx.compose.animation.core.spring
|
|
||||||
import androidx.compose.foundation.Canvas
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.systemBars
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.geometry.CornerRadius
|
|
||||||
import androidx.compose.ui.geometry.RoundRect
|
|
||||||
import androidx.compose.ui.geometry.toRect
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.Path
|
|
||||||
import androidx.compose.ui.graphics.PathEffect
|
|
||||||
import androidx.compose.ui.graphics.PathMeasure
|
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun CustomBottomNavbar(tabs: List<BottomNavItem>, navBarState: NavBarState) {
|
|
||||||
var selectedTabIndex by remember { mutableIntStateOf(0) }
|
|
||||||
var isChildRoute by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
LaunchedEffect(tabs) {
|
|
||||||
}
|
|
||||||
when (navBarState.route) {
|
|
||||||
Route.Main -> {
|
|
||||||
selectedTabIndex = 0
|
|
||||||
isChildRoute = false
|
|
||||||
}
|
|
||||||
Route.AutoTunnel -> {
|
|
||||||
selectedTabIndex = 1
|
|
||||||
isChildRoute = false
|
|
||||||
}
|
|
||||||
Route.Settings -> {
|
|
||||||
selectedTabIndex = 2
|
|
||||||
isChildRoute = false
|
|
||||||
}
|
|
||||||
Route.Support -> {
|
|
||||||
selectedTabIndex = 3
|
|
||||||
isChildRoute = false
|
|
||||||
}
|
|
||||||
else -> isChildRoute = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val systemBars = WindowInsets.systemBars
|
|
||||||
val bottomPadding = with(LocalDensity.current) {
|
|
||||||
systemBars.getBottom(this).toDp()
|
|
||||||
}
|
|
||||||
val navHeight = 64.dp + bottomPadding
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(navHeight)
|
|
||||||
.background(Color.Transparent),
|
|
||||||
) {
|
|
||||||
BottomBarTabs(
|
|
||||||
tabs = tabs,
|
|
||||||
selectedTabIndex = selectedTabIndex,
|
|
||||||
isChildRoute = isChildRoute,
|
|
||||||
onTabSelected = {
|
|
||||||
selectedTabIndex = tabs.indexOf(it)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
val animatedSelectedTabIndex by animateFloatAsState(
|
|
||||||
targetValue = selectedTabIndex.toFloat(),
|
|
||||||
label = "animatedSelectedTabIndex",
|
|
||||||
animationSpec = spring(
|
|
||||||
stiffness = Spring.StiffnessLow,
|
|
||||||
dampingRatio = Spring.DampingRatioLowBouncy,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
val animatedColor by animateColorAsState(
|
|
||||||
targetValue = MaterialTheme.colorScheme.primary,
|
|
||||||
label = "animatedColor",
|
|
||||||
animationSpec = spring(
|
|
||||||
stiffness = Spring.StiffnessLow,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
Canvas(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(navHeight),
|
|
||||||
) {
|
|
||||||
val path = Path().apply {
|
|
||||||
addRoundRect(RoundRect(size.toRect(), CornerRadius(size.height)))
|
|
||||||
}
|
|
||||||
val length = PathMeasure().apply { setPath(path, false) }.length
|
|
||||||
|
|
||||||
val tabWidth = size.width / tabs.size
|
|
||||||
drawPath(
|
|
||||||
path,
|
|
||||||
brush = Brush.horizontalGradient(
|
|
||||||
colors = listOf(
|
|
||||||
animatedColor.copy(alpha = 0f),
|
|
||||||
animatedColor.copy(alpha = 1f),
|
|
||||||
animatedColor.copy(alpha = 1f),
|
|
||||||
animatedColor.copy(alpha = 0f),
|
|
||||||
),
|
|
||||||
startX = tabWidth * animatedSelectedTabIndex,
|
|
||||||
endX = tabWidth * (animatedSelectedTabIndex + 1),
|
|
||||||
),
|
|
||||||
style = Stroke(
|
|
||||||
width = 4f,
|
|
||||||
pathEffect = PathEffect.dashPathEffect(
|
|
||||||
intervals = floatArrayOf(length / 2, length),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-47
@@ -1,47 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.common.navigation
|
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.animation.slideInVertically
|
|
||||||
import androidx.compose.animation.slideOutVertically
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun DynamicTopAppBar(navBarState: NavBarState, modifier: Modifier = Modifier) {
|
|
||||||
TopAppBar(
|
|
||||||
modifier = modifier,
|
|
||||||
colors = TopAppBarDefaults.topAppBarColors().copy(Color.Transparent),
|
|
||||||
title = {
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = navBarState.showTop,
|
|
||||||
enter = slideInVertically() + fadeIn(),
|
|
||||||
exit = slideOutVertically() + fadeOut(),
|
|
||||||
) {
|
|
||||||
Box(modifier = Modifier.padding(start = 10.dp)) {
|
|
||||||
navBarState.topTitle?.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = navBarState.showTop,
|
|
||||||
enter = slideInVertically() + fadeIn(),
|
|
||||||
exit = slideOutVertically() + fadeOut(),
|
|
||||||
) {
|
|
||||||
Box(modifier = Modifier.padding(end = 10.dp)) {
|
|
||||||
navBarState.topTrailing?.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-217
@@ -1,217 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.common.navigation
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.rounded.Add
|
|
||||||
import androidx.compose.material.icons.rounded.Edit
|
|
||||||
import androidx.compose.material.icons.rounded.Menu
|
|
||||||
import androidx.compose.material.icons.rounded.PlayArrow
|
|
||||||
import androidx.compose.material.icons.rounded.Save
|
|
||||||
import androidx.compose.material.icons.rounded.Stop
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.State
|
|
||||||
import androidx.compose.runtime.produceState
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.navigation.NavBackStackEntry
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.navigation.toRoute
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Brick
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
|
||||||
|
|
||||||
data class NavBarState(
|
|
||||||
val showTop: Boolean = true,
|
|
||||||
val showBottom: Boolean = true,
|
|
||||||
val topTitle: @Composable (() -> Unit)? = null,
|
|
||||||
val topTrailing: @Composable (() -> Unit)? = null,
|
|
||||||
val route: Route? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun currentNavBackStackEntryAsNavBarState(
|
|
||||||
navController: NavController,
|
|
||||||
backStackEntry: NavBackStackEntry?,
|
|
||||||
viewModel: AppViewModel,
|
|
||||||
uiState: AppUiState,
|
|
||||||
): State<NavBarState> {
|
|
||||||
return produceState(initialValue = NavBarState(), key1 = backStackEntry, key2 = uiState) {
|
|
||||||
value = when {
|
|
||||||
backStackEntry.isCurrentRoute(Route.Main::class) -> {
|
|
||||||
NavBarState(
|
|
||||||
showTop = true, showBottom = true,
|
|
||||||
{ Text(stringResource(R.string.tunnels)) },
|
|
||||||
{
|
|
||||||
IconButton(
|
|
||||||
onClick = { viewModel.handleEvent(AppEvent.ToggleBottomSheet) },
|
|
||||||
) {
|
|
||||||
val icon = Icons.Rounded.Add
|
|
||||||
Icon(icon, stringResource(R.string.add_tunnel), modifier = Modifier.size(iconSize))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
route = Route.Main,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
backStackEntry.isCurrentRoute(Route.AutoTunnel::class) -> {
|
|
||||||
NavBarState(
|
|
||||||
showTop = true, showBottom = true,
|
|
||||||
{ Text(stringResource(R.string.auto_tunnel)) },
|
|
||||||
{
|
|
||||||
IconButton(
|
|
||||||
onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnel) },
|
|
||||||
) {
|
|
||||||
val (icon, description, color) = if (uiState.appSettings.isAutoTunnelEnabled) {
|
|
||||||
Triple(Icons.Rounded.Stop, R.string.stop_auto, Brick)
|
|
||||||
} else {
|
|
||||||
Triple(Icons.Rounded.PlayArrow, R.string.start_auto, SilverTree)
|
|
||||||
}
|
|
||||||
Icon(icon, stringResource(description), tint = color, modifier = Modifier.size(iconSize))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
route = Route.AutoTunnel,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
backStackEntry.isCurrentRoute(Route.AutoTunnelAdvanced::class) -> {
|
|
||||||
NavBarState(
|
|
||||||
showTop = true, showBottom = true,
|
|
||||||
{ Text(stringResource(R.string.advanced_settings)) },
|
|
||||||
route = Route.AutoTunnelAdvanced,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
backStackEntry.isCurrentRoute(Route.Settings::class) -> {
|
|
||||||
NavBarState(
|
|
||||||
showTop = true, showBottom = true,
|
|
||||||
{ Text(stringResource(R.string.settings)) },
|
|
||||||
{
|
|
||||||
IconButton(
|
|
||||||
onClick = { viewModel.handleEvent(AppEvent.ToggleBottomSheet) },
|
|
||||||
) {
|
|
||||||
val icon = Icons.Rounded.Menu
|
|
||||||
Icon(icon, stringResource(R.string.quick_actions), modifier = Modifier.size(iconSize))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
route = Route.Settings,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
backStackEntry.isCurrentRoute(Route.KillSwitch::class) -> {
|
|
||||||
NavBarState(
|
|
||||||
showTop = true, showBottom = true,
|
|
||||||
{ Text(stringResource(R.string.kill_switch)) },
|
|
||||||
route = Route.KillSwitch,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
backStackEntry.isCurrentRoute(Route.Appearance::class) -> {
|
|
||||||
NavBarState(
|
|
||||||
showTop = true, showBottom = true,
|
|
||||||
{ Text(stringResource(R.string.appearance)) },
|
|
||||||
route = Route.Appearance,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
backStackEntry.isCurrentRoute(Route.Language::class) -> {
|
|
||||||
NavBarState(
|
|
||||||
showTop = true, showBottom = true,
|
|
||||||
{ Text(stringResource(R.string.language)) },
|
|
||||||
route = Route.Language,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
backStackEntry.isCurrentRoute(Route.Display::class) -> {
|
|
||||||
NavBarState(
|
|
||||||
showTop = true, showBottom = true,
|
|
||||||
{ Text(stringResource(R.string.display_theme)) },
|
|
||||||
route = Route.Display,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
backStackEntry.isCurrentRoute(Route.Logs::class) -> {
|
|
||||||
NavBarState(
|
|
||||||
showTop = true, showBottom = false,
|
|
||||||
{ Text(stringResource(R.string.logs)) },
|
|
||||||
{
|
|
||||||
IconButton(
|
|
||||||
onClick = { viewModel.handleEvent(AppEvent.ToggleBottomSheet) },
|
|
||||||
) {
|
|
||||||
val icon = Icons.Rounded.Menu
|
|
||||||
Icon(icon, stringResource(R.string.quick_actions), modifier = Modifier.size(iconSize))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
route = Route.Logs,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
backStackEntry.isCurrentRoute(Route.TunnelOptions::class) -> {
|
|
||||||
val args = backStackEntry?.toRoute<Route.TunnelOptions>()
|
|
||||||
val tunnel = uiState.tunnels.find { it.id == args?.id }
|
|
||||||
NavBarState(
|
|
||||||
showTop = true, showBottom = true,
|
|
||||||
{ tunnel?.name?.let { Text(it) } },
|
|
||||||
{
|
|
||||||
IconButton(
|
|
||||||
onClick = { tunnel?.id?.let { navController.navigate(Route.Config(id = it)) } },
|
|
||||||
) {
|
|
||||||
val icon = Icons.Rounded.Edit
|
|
||||||
Icon(icon, stringResource(R.string.edit_tunnel), modifier = Modifier.size(iconSize))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
route = args?.let { Route.TunnelOptions(it.id) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
backStackEntry.isCurrentRoute(Route.SplitTunnel::class) -> {
|
|
||||||
val args = backStackEntry?.toRoute<Route.SplitTunnel>()
|
|
||||||
val name = uiState.tunnels.find { it.id == args?.id }?.name
|
|
||||||
NavBarState(
|
|
||||||
showTop = true, showBottom = true,
|
|
||||||
{ name?.let { Text(it) } },
|
|
||||||
{
|
|
||||||
IconButton(
|
|
||||||
onClick = { viewModel.handleEvent(AppEvent.InvokeScreenAction) },
|
|
||||||
) {
|
|
||||||
val icon = Icons.Rounded.Save
|
|
||||||
Icon(icon, stringResource(R.string.save), modifier = Modifier.size(iconSize))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
route = args?.let { Route.SplitTunnel(it.id) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
backStackEntry.isCurrentRoute(Route.Config::class) -> {
|
|
||||||
val args = backStackEntry?.toRoute<Route.Config>()
|
|
||||||
val name = uiState.tunnels.find { it.id == args?.id }?.name
|
|
||||||
NavBarState(
|
|
||||||
showTop = true, showBottom = true,
|
|
||||||
{ name?.let { Text(it) } },
|
|
||||||
{
|
|
||||||
IconButton(
|
|
||||||
onClick = { viewModel.handleEvent(AppEvent.InvokeScreenAction) },
|
|
||||||
) {
|
|
||||||
val icon = Icons.Rounded.Save
|
|
||||||
Icon(icon, stringResource(R.string.save), modifier = Modifier.size(iconSize))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
route = args?.let { Route.Config(it.id) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
backStackEntry.isCurrentRoute(Route.TunnelAutoTunnel::class) -> {
|
|
||||||
val args = backStackEntry?.toRoute<Route.TunnelAutoTunnel>()
|
|
||||||
val name = uiState.tunnels.find { it.id == args?.id }?.name
|
|
||||||
NavBarState(
|
|
||||||
showTop = true, showBottom = true,
|
|
||||||
{ name?.let { Text(it) } },
|
|
||||||
route = args?.let { Route.TunnelAutoTunnel(it.id) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
backStackEntry.isCurrentRoute(Route.Support::class) -> {
|
|
||||||
NavBarState(
|
|
||||||
showTop = true, showBottom = true,
|
|
||||||
{ Text(stringResource(R.string.support)) },
|
|
||||||
route = Route.Support,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> NavBarState(showTop = false, showBottom = false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common.navigation
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
||||||
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun TopNavBar(title: String, trailing: @Composable () -> Unit = {}, showBack: Boolean = true) {
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
CenterAlignedTopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(title)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
if (showBack) {
|
||||||
|
IconButton(onClick = {
|
||||||
|
navController.popBackStack()
|
||||||
|
}) {
|
||||||
|
val icon = Icons.AutoMirrored.Outlined.ArrowBack
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = icon.name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
trailing()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
+2
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.common.dialog
|
package com.zaneschepke.wireguardautotunnel.ui.common.permission.vpn
|
||||||
|
|
||||||
import androidx.compose.foundation.text.ClickableText
|
import androidx.compose.foundation.text.ClickableText
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -10,6 +10,7 @@ import androidx.compose.ui.text.SpanStyle
|
|||||||
import androidx.compose.ui.text.buildAnnotatedString
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.withStyle
|
import androidx.compose.ui.text.withStyle
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchVpnSettings
|
import com.zaneschepke.wireguardautotunnel.util.extensions.launchVpnSettings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common.permission.vpn
|
||||||
|
|
||||||
|
import android.net.VpnService
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
inline fun <T> withVpnPermission(crossinline onSuccess: (t: T) -> Unit): (t: T) -> Unit {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
var showVpnPermissionDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val vpnActivity =
|
||||||
|
rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult(),
|
||||||
|
onResult = {
|
||||||
|
if (it.resultCode != RESULT_OK) showVpnPermissionDialog = true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
VpnDeniedDialog(showVpnPermissionDialog, onDismiss = { showVpnPermissionDialog = false })
|
||||||
|
|
||||||
|
return {
|
||||||
|
val intent = VpnService.prepare(context)
|
||||||
|
if (intent != null) {
|
||||||
|
vpnActivity.launch(intent)
|
||||||
|
} else {
|
||||||
|
onSuccess(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common.permission
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isBatteryOptimizationsDisabled
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
inline fun withIgnoreBatteryOpt(ignore: Boolean, crossinline callback: () -> Unit): () -> Unit {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val batteryActivity =
|
||||||
|
rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult(),
|
||||||
|
) { result: ActivityResult ->
|
||||||
|
// we only ask once
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
if (ignore || context.isBatteryOptimizationsDisabled()) {
|
||||||
|
callback()
|
||||||
|
} else {
|
||||||
|
batteryActivity.launch(
|
||||||
|
Intent().apply {
|
||||||
|
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
||||||
|
data = Uri.parse("package:${context.packageName}")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
-9
@@ -9,7 +9,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AuthorizationPrompt(onSuccess: () -> Unit, onFailure: () -> Unit, onError: (String) -> Unit) {
|
fun AuthorizationPrompt(onSuccess: () -> Unit, onFailure: () -> Unit, onError: (String) -> Unit) {
|
||||||
@@ -19,20 +18,33 @@ fun AuthorizationPrompt(onSuccess: () -> Unit, onFailure: () -> Unit, onError: (
|
|||||||
val isBiometricAvailable =
|
val isBiometricAvailable =
|
||||||
remember {
|
remember {
|
||||||
when (bio) {
|
when (bio) {
|
||||||
|
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
|
||||||
|
onError("Biometrics not available")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
|
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
|
||||||
onError(context.getString(R.string.bio_not_created))
|
onError("Biometrics not created")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
|
||||||
|
onError("Biometric hardware not found")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
|
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
|
||||||
onError(context.getString(R.string.bio_update_required))
|
onError("Biometric security update required")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED, BiometricManager.BIOMETRIC_STATUS_UNKNOWN,
|
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
|
||||||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE, BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE,
|
onError("Biometrics not supported")
|
||||||
-> {
|
false
|
||||||
onError(context.getString(R.string.bio_not_supported))
|
}
|
||||||
|
|
||||||
|
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
|
||||||
|
onError("Biometrics status unknown")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,8 +58,8 @@ fun AuthorizationPrompt(onSuccess: () -> Unit, onFailure: () -> Unit, onError: (
|
|||||||
val promptInfo =
|
val promptInfo =
|
||||||
BiometricPrompt.PromptInfo.Builder()
|
BiometricPrompt.PromptInfo.Builder()
|
||||||
.setAllowedAuthenticators(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
|
.setAllowedAuthenticators(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
|
||||||
.setTitle(context.getString(R.string.bio_auth_title))
|
.setTitle("Biometric Authentication")
|
||||||
.setSubtitle(context.getString(R.string.bio_subtitle))
|
.setSubtitle("Log in using your biometric credential")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val biometricPrompt =
|
val biometricPrompt =
|
||||||
|
|||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
|
||||||
|
|
||||||
|
import androidx.compose.material3.SnackbarDuration
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.SnackbarResult
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
|
private val LocalSnackbarController = staticCompositionLocalOf {
|
||||||
|
SnackbarController(
|
||||||
|
host = SnackbarHostState(),
|
||||||
|
scope = CoroutineScope(EmptyCoroutineContext),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
private val channel = Channel<SnackbarChannelMessage>(capacity = 1)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SnackbarControllerProvider(content: @Composable (snackbarHost: SnackbarHostState) -> Unit) {
|
||||||
|
val snackHostState = remember { SnackbarHostState() }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val snackController = remember(scope) { SnackbarController(snackHostState, scope) }
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
DisposableEffect(snackController, scope) {
|
||||||
|
val job = scope.launch {
|
||||||
|
for (payload in channel) {
|
||||||
|
snackController.showMessage(
|
||||||
|
message = payload.message.asString(context),
|
||||||
|
duration = payload.duration,
|
||||||
|
action = payload.action,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDispose {
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompositionLocalProvider(LocalSnackbarController provides snackController) {
|
||||||
|
content(
|
||||||
|
snackHostState,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class SnackbarController(
|
||||||
|
private val host: SnackbarHostState,
|
||||||
|
private val scope: CoroutineScope,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
val current
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
get() = LocalSnackbarController.current
|
||||||
|
|
||||||
|
fun showMessage(message: StringValue, action: SnackbarAction? = null, duration: SnackbarDuration = SnackbarDuration.Short) {
|
||||||
|
channel.trySend(
|
||||||
|
SnackbarChannelMessage(
|
||||||
|
message = message,
|
||||||
|
duration = duration,
|
||||||
|
action = action,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showMessage(message: String, action: SnackbarAction? = null, duration: SnackbarDuration = SnackbarDuration.Short) {
|
||||||
|
scope.launch {
|
||||||
|
/**
|
||||||
|
* note: uncomment this line if you want snackbar to be displayed immediately,
|
||||||
|
* rather than being enqueued and waiting [duration] * current_queue_size
|
||||||
|
*/
|
||||||
|
host.currentSnackbarData?.dismiss()
|
||||||
|
val result =
|
||||||
|
host.showSnackbar(
|
||||||
|
message = message,
|
||||||
|
actionLabel = action?.title,
|
||||||
|
duration = duration,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result == SnackbarResult.ActionPerformed) {
|
||||||
|
action?.onActionPress?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SnackbarChannelMessage(
|
||||||
|
val message: StringValue,
|
||||||
|
val action: SnackbarAction?,
|
||||||
|
val duration: SnackbarDuration = SnackbarDuration.Short,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SnackbarAction(val title: String, val onActionPress: () -> Unit)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common.text
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SectionTitle(title: String, padding: Dp) {
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.padding(padding, bottom = 5.dp, top = 5.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.state
|
package com.zaneschepke.wireguardautotunnel.ui.enums
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
@@ -8,7 +8,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
|
|
||||||
enum class SplitOption {
|
enum class SplitOptions {
|
||||||
INCLUDE,
|
INCLUDE,
|
||||||
ALL,
|
ALL,
|
||||||
EXCLUDE,
|
EXCLUDE,
|
||||||
-109
@@ -1,109 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
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.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
|
||||||
import com.google.accompanist.permissions.isGranted
|
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.AdvancedSettingsItem
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.NetworkTunnelingItems
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.WifiTunnelingItems
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackgroundLocationDialog
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LocationServicesDialog
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isLocationServicesEnabled
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
|
||||||
|
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
|
||||||
@Composable
|
|
||||||
fun AutoTunnelScreen(uiState: AppUiState, viewModel: AppViewModel) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
|
||||||
var currentText by remember { mutableStateOf("") }
|
|
||||||
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
|
|
||||||
var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
|
|
||||||
var showLocationDialog by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
fun checkFineLocationGranted() {
|
|
||||||
isBackgroundLocationGranted = fineLocationState.status.isGranted
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isWifiNameReadable(): Boolean {
|
|
||||||
return when {
|
|
||||||
!isBackgroundLocationGranted || !fineLocationState.status.isGranted -> {
|
|
||||||
showLocationDialog = true
|
|
||||||
false
|
|
||||||
}
|
|
||||||
!context.isLocationServicesEnabled() -> {
|
|
||||||
showLocationServicesAlertDialog = true
|
|
||||||
false
|
|
||||||
}
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) checkFineLocationGranted()
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
if (context.isRunningOnTv() && Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
|
|
||||||
checkFineLocationGranted()
|
|
||||||
} else {
|
|
||||||
val backgroundLocationState = rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
|
||||||
isBackgroundLocationGranted = backgroundLocationState.status.isGranted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(uiState.appSettings.trustedNetworkSSIDs) {
|
|
||||||
currentText = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
LocationServicesDialog(
|
|
||||||
showLocationServicesAlertDialog,
|
|
||||||
onDismiss = { showLocationServicesAlertDialog = false },
|
|
||||||
onAttest = { showLocationServicesAlertDialog = false },
|
|
||||||
)
|
|
||||||
|
|
||||||
BackgroundLocationDialog(
|
|
||||||
showLocationDialog,
|
|
||||||
onDismiss = { showLocationDialog = false },
|
|
||||||
onAttest = { showLocationDialog = false },
|
|
||||||
)
|
|
||||||
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.padding(vertical = 24.dp)
|
|
||||||
.padding(horizontal = 24.dp),
|
|
||||||
) {
|
|
||||||
SurfaceSelectionGroupButton(
|
|
||||||
items = WifiTunnelingItems(uiState, viewModel, currentText, { currentText = it }, { isWifiNameReadable() }),
|
|
||||||
)
|
|
||||||
SurfaceSelectionGroupButton(
|
|
||||||
items = NetworkTunnelingItems(uiState, viewModel),
|
|
||||||
)
|
|
||||||
SurfaceSelectionGroupButton(
|
|
||||||
items = listOf(AdvancedSettingsItem()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-37
@@ -1,37 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced.components.DebounceDelaySelector
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun AdvancedScreen(appUiState: AppUiState) {
|
|
||||||
val appViewModel: AppViewModel = hiltViewModel()
|
|
||||||
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.padding(top = 24.dp)
|
|
||||||
.padding(horizontal = 24.dp),
|
|
||||||
) {
|
|
||||||
DebounceDelaySelector(
|
|
||||||
currentDelay = appUiState.appSettings.debounceDelaySeconds,
|
|
||||||
onEvent = appViewModel::handleEvent,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-48
@@ -1,48 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced.components
|
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.PauseCircle
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.DropdownSelector
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun DebounceDelaySelector(currentDelay: Int, onEvent: (AppEvent) -> Unit) {
|
|
||||||
var isDropDownExpanded by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
SurfaceSelectionGroupButton(
|
|
||||||
listOf(
|
|
||||||
SelectionItem(
|
|
||||||
leadingIcon = Icons.Outlined.PauseCircle,
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.debounce_delay),
|
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onClick = { isDropDownExpanded = true },
|
|
||||||
trailing = {
|
|
||||||
DropdownSelector(
|
|
||||||
currentValue = currentDelay,
|
|
||||||
options = (0..10).toList(),
|
|
||||||
onValueSelected = { num -> onEvent(AppEvent.SetDebounceDelay(num)) },
|
|
||||||
isExpanded = isDropDownExpanded,
|
|
||||||
onDismiss = { isDropDownExpanded = false },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-31
@@ -1,31 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components
|
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun AdvancedSettingsItem(): SelectionItem {
|
|
||||||
val navController = LocalNavController.current
|
|
||||||
return SelectionItem(
|
|
||||||
leadingIcon = Icons.Outlined.Settings,
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.advanced_settings),
|
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
trailing = {
|
|
||||||
ForwardButton { navController.navigate(Route.AutoTunnelAdvanced) }
|
|
||||||
},
|
|
||||||
onClick = { navController.navigate(Route.AutoTunnelAdvanced) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-98
@@ -1,98 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components
|
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.AirplanemodeActive
|
|
||||||
import androidx.compose.material.icons.outlined.SettingsEthernet
|
|
||||||
import androidx.compose.material.icons.outlined.SignalCellular4Bar
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun NetworkTunnelingItems(uiState: AppUiState, viewModel: AppViewModel): List<SelectionItem> {
|
|
||||||
return listOf(
|
|
||||||
SelectionItem(
|
|
||||||
leadingIcon = Icons.Outlined.SignalCellular4Bar,
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.tunnel_mobile_data),
|
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
trailing = {
|
|
||||||
ScaledSwitch(
|
|
||||||
enabled = !uiState.appSettings.isAlwaysOnVpnEnabled,
|
|
||||||
checked = uiState.appSettings.isTunnelOnMobileDataEnabled,
|
|
||||||
onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnCellular) },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
description = {
|
|
||||||
val cellularActive = remember(uiState.networkStatus) { uiState.networkStatus?.cellularConnected ?: false }
|
|
||||||
Text(
|
|
||||||
text = if (cellularActive) stringResource(R.string.active) else stringResource(R.string.inactive),
|
|
||||||
style = MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.outline),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnCellular) },
|
|
||||||
),
|
|
||||||
SelectionItem(
|
|
||||||
leadingIcon = Icons.Outlined.SettingsEthernet,
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.tunnel_on_ethernet),
|
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
trailing = {
|
|
||||||
ScaledSwitch(
|
|
||||||
enabled = !uiState.appSettings.isAlwaysOnVpnEnabled,
|
|
||||||
checked = uiState.appSettings.isTunnelOnEthernetEnabled,
|
|
||||||
onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnEthernet) },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
description = {
|
|
||||||
val ethernetActive = remember(uiState.networkStatus) { uiState.networkStatus?.ethernetConnected ?: false }
|
|
||||||
Text(
|
|
||||||
text = if (ethernetActive) stringResource(R.string.active) else stringResource(R.string.inactive),
|
|
||||||
style = MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.outline),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnEthernet) },
|
|
||||||
),
|
|
||||||
SelectionItem(
|
|
||||||
leadingIcon = Icons.Outlined.AirplanemodeActive,
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.stop_on_no_internet),
|
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
description = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.stop_on_internet_loss),
|
|
||||||
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
trailing = {
|
|
||||||
ScaledSwitch(
|
|
||||||
checked = uiState.appSettings.isStopOnNoInternetEnabled,
|
|
||||||
onClick = { viewModel.handleEvent(AppEvent.ToggleStopTunnelOnNoInternet) },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onClick = { viewModel.handleEvent(AppEvent.ToggleStopTunnelOnNoInternet) },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-196
@@ -1,196 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.Code
|
|
||||||
import androidx.compose.material.icons.outlined.Filter1
|
|
||||||
import androidx.compose.material.icons.outlined.Security
|
|
||||||
import androidx.compose.material.icons.outlined.VpnKeyOff
|
|
||||||
import androidx.compose.material.icons.outlined.Wifi
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.zaneschepke.networkmonitor.NetworkStatus
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LearnMoreLinkLabel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun WifiTunnelingItems(
|
|
||||||
uiState: AppUiState,
|
|
||||||
viewModel: AppViewModel,
|
|
||||||
currentText: String,
|
|
||||||
onTextChange: (String) -> Unit,
|
|
||||||
isWifiNameReadable: () -> Boolean,
|
|
||||||
): List<SelectionItem> {
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
val baseItems = listOf(
|
|
||||||
SelectionItem(
|
|
||||||
leadingIcon = Icons.Outlined.Wifi,
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.tunnel_on_wifi),
|
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
trailing = {
|
|
||||||
ScaledSwitch(
|
|
||||||
enabled = !uiState.appSettings.isAlwaysOnVpnEnabled,
|
|
||||||
checked = uiState.appSettings.isTunnelOnWifiEnabled,
|
|
||||||
onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnWifi) },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
description = {
|
|
||||||
val wifiName by remember(uiState.networkStatus) {
|
|
||||||
derivedStateOf {
|
|
||||||
(uiState.networkStatus as? NetworkStatus.Connected)
|
|
||||||
?.takeIf { it.wifiConnected }
|
|
||||||
?.wifiSsid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
text = wifiName?.let { stringResource(R.string.wifi_name_template, it) } ?: stringResource(R.string.inactive),
|
|
||||||
style = MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.outline),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnWifi) },
|
|
||||||
),
|
|
||||||
SelectionItem(
|
|
||||||
leadingIcon = Icons.Outlined.Code,
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.wifi_name_via_shell),
|
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
description = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.use_root_shell_for_wifi),
|
|
||||||
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
trailing = {
|
|
||||||
ScaledSwitch(
|
|
||||||
checked = uiState.appSettings.isWifiNameByShellEnabled,
|
|
||||||
onClick = { viewModel.handleEvent(AppEvent.ToggleRootShellWifi) },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onClick = { viewModel.handleEvent(AppEvent.ToggleRootShellWifi) },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return if (uiState.appSettings.isTunnelOnWifiEnabled) {
|
|
||||||
baseItems + listOf(
|
|
||||||
SelectionItem(
|
|
||||||
leadingIcon = Icons.Outlined.Filter1,
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.use_wildcards),
|
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
description = {
|
|
||||||
LearnMoreLinkLabel({ context.openWebUrl(it) }, stringResource(R.string.docs_wildcards))
|
|
||||||
},
|
|
||||||
trailing = {
|
|
||||||
ScaledSwitch(
|
|
||||||
checked = uiState.appSettings.isWildcardsEnabled,
|
|
||||||
onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelWildcards) },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelWildcards) },
|
|
||||||
),
|
|
||||||
SelectionItem(
|
|
||||||
title = {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.weight(4f, false).fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
val icon = Icons.Outlined.Security
|
|
||||||
Icon(
|
|
||||||
icon,
|
|
||||||
icon.name,
|
|
||||||
modifier = Modifier.size(iconSize),
|
|
||||||
)
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = 16.dp)
|
|
||||||
.padding(vertical = 6.dp),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.trusted_wifi_names),
|
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
description = {
|
|
||||||
TrustedNetworkTextBox(
|
|
||||||
uiState.appSettings.trustedNetworkSSIDs,
|
|
||||||
onDelete = { viewModel.handleEvent(AppEvent.DeleteTrustedSSID(it)) },
|
|
||||||
currentText = currentText,
|
|
||||||
onSave = { ssid ->
|
|
||||||
if (uiState.appSettings.isWifiNameByShellEnabled || isWifiNameReadable()) {
|
|
||||||
viewModel.handleEvent(AppEvent.SaveTrustedSSID(ssid))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onValueChange = onTextChange,
|
|
||||||
supporting = { if (uiState.appSettings.isWildcardsEnabled) WildcardsLabel() },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SelectionItem(
|
|
||||||
leadingIcon = Icons.Outlined.VpnKeyOff,
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.kill_switch_off),
|
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
trailing = {
|
|
||||||
ScaledSwitch(
|
|
||||||
enabled = uiState.appSettings.isVpnKillSwitchEnabled,
|
|
||||||
checked = uiState.appSettings.isDisableKillSwitchOnTrustedEnabled,
|
|
||||||
onClick = { viewModel.handleEvent(AppEvent.ToggleStopKillSwitchOnTrusted) },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onClick = { viewModel.handleEvent(AppEvent.ToggleStopKillSwitchOnTrusted) },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
baseItems
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+808
@@ -0,0 +1,808 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.focusGroup
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Save
|
||||||
|
import androidx.compose.material.icons.rounded.ContentCopy
|
||||||
|
import androidx.compose.material.icons.rounded.Delete
|
||||||
|
import androidx.compose.material.icons.rounded.MoreVert
|
||||||
|
import androidx.compose.material.icons.rounded.Refresh
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
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.setValue
|
||||||
|
import androidx.compose.runtime.toMutableStateList
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.platform.ClipboardManager
|
||||||
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
|
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.enums.InterfaceActions
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.enums.PeerActions
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.state.PeerProxy
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||||
|
import org.amnezia.awg.crypto.KeyPair
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ConfigScreen(tunnelConf: TunnelConf?, appViewModel: AppViewModel) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val snackbar = SnackbarController.current
|
||||||
|
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
|
||||||
|
var isInterfaceDropDownExpanded by remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
val popBackStack by appViewModel.popBackStack.collectAsStateWithLifecycle(false)
|
||||||
|
|
||||||
|
val configPair = Pair(tunnelConf?.tunName ?: "", tunnelConf?.toAmConfig())
|
||||||
|
|
||||||
|
var tunnelName by remember {
|
||||||
|
mutableStateOf(configPair.first)
|
||||||
|
}
|
||||||
|
|
||||||
|
var interfaceState by remember {
|
||||||
|
mutableStateOf(configPair.second?.let { InterfaceProxy.from(it.`interface`) } ?: InterfaceProxy())
|
||||||
|
}
|
||||||
|
|
||||||
|
var showAmneziaValues by remember {
|
||||||
|
mutableStateOf(configPair.second?.`interface`?.junkPacketCount?.isPresent == true)
|
||||||
|
}
|
||||||
|
|
||||||
|
var showScripts by remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
val peersState = remember {
|
||||||
|
(configPair.second?.peers?.map { PeerProxy.from(it) } ?: listOf(PeerProxy())).toMutableStateList()
|
||||||
|
}
|
||||||
|
|
||||||
|
var showAuthPrompt by remember { mutableStateOf(false) }
|
||||||
|
var isAuthenticated by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() })
|
||||||
|
val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done)
|
||||||
|
|
||||||
|
if (showAuthPrompt) {
|
||||||
|
AuthorizationPrompt(
|
||||||
|
onSuccess = {
|
||||||
|
showAuthPrompt = false
|
||||||
|
isAuthenticated = true
|
||||||
|
},
|
||||||
|
onError = {
|
||||||
|
showAuthPrompt = false
|
||||||
|
snackbar.showMessage(
|
||||||
|
context.getString(R.string.error_authentication_failed),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
showAuthPrompt = false
|
||||||
|
snackbar.showMessage(
|
||||||
|
context.getString(R.string.error_authorization_failed),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(popBackStack) {
|
||||||
|
if (popBackStack) navController.popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopNavBar(stringResource(R.string.edit_tunnel), trailing = {
|
||||||
|
IconButton(onClick = {
|
||||||
|
tunnelConf?.let {
|
||||||
|
appViewModel.updateExistingTunnelConfig(
|
||||||
|
it,
|
||||||
|
tunnelName,
|
||||||
|
peersState,
|
||||||
|
interfaceState,
|
||||||
|
)
|
||||||
|
} ?: appViewModel.saveNewTunnel(tunnelName, peersState, interfaceState)
|
||||||
|
}) {
|
||||||
|
val icon = Icons.Outlined.Save
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = icon.name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
|
.imePadding()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(top = 24.dp.scaledHeight())
|
||||||
|
.padding(horizontal = 24.dp.scaledWidth()),
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(5.dp, Alignment.Top),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp.scaledWidth())
|
||||||
|
.focusGroup(),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier =
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
GroupLabel(
|
||||||
|
stringResource(R.string.interface_),
|
||||||
|
)
|
||||||
|
Column {
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.size(iconSize),
|
||||||
|
onClick = {
|
||||||
|
isInterfaceDropDownExpanded = true
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
val icon = Icons.Rounded.MoreVert
|
||||||
|
Icon(icon, icon.name)
|
||||||
|
}
|
||||||
|
DropdownMenu(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
expanded = isInterfaceDropDownExpanded,
|
||||||
|
modifier = Modifier.shadow(12.dp).background(MaterialTheme.colorScheme.surface),
|
||||||
|
onDismissRequest = {
|
||||||
|
isInterfaceDropDownExpanded = false
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
val isAmneziaCompatibilitySet = interfaceState.isAmneziaCompatibilityModeSet()
|
||||||
|
InterfaceActions.entries.forEach { action ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = when (action) {
|
||||||
|
InterfaceActions.TOGGLE_SHOW_SCRIPTS -> if (showScripts) {
|
||||||
|
stringResource(R.string.hide_scripts)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.show_scripts)
|
||||||
|
}
|
||||||
|
InterfaceActions.TOGGLE_AMNEZIA_VALUES -> if (showAmneziaValues) {
|
||||||
|
stringResource(R.string.hide_amnezia_properties)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.show_amnezia_properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
InterfaceActions.SET_AMNEZIA_COMPATIBILITY -> if (isAmneziaCompatibilitySet) {
|
||||||
|
stringResource(R.string.remove_amnezia_compatibility)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.enable_amnezia_compatibility)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
isInterfaceDropDownExpanded = false
|
||||||
|
when (action) {
|
||||||
|
InterfaceActions.TOGGLE_AMNEZIA_VALUES -> showAmneziaValues = !showAmneziaValues
|
||||||
|
InterfaceActions.TOGGLE_SHOW_SCRIPTS -> showScripts = !showScripts
|
||||||
|
InterfaceActions.SET_AMNEZIA_COMPATIBILITY -> if (isAmneziaCompatibilitySet) {
|
||||||
|
showAmneziaValues = false
|
||||||
|
interfaceState = interfaceState.resetAmneziaProperties()
|
||||||
|
} else {
|
||||||
|
showAmneziaValues = true
|
||||||
|
interfaceState = interfaceState.toAmneziaCompatibilityConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = tunnelName,
|
||||||
|
onValueChange = { tunnelName = it },
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.name),
|
||||||
|
hint = stringResource(R.string.tunnel_name).lowercase(),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
val privateKeyEnabled = (tunnelConf == null) || isAuthenticated
|
||||||
|
OutlinedTextField(
|
||||||
|
textStyle = MaterialTheme.typography.labelLarge,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { showAuthPrompt = true },
|
||||||
|
value = interfaceState.privateKey,
|
||||||
|
visualTransformation =
|
||||||
|
if (privateKeyEnabled) {
|
||||||
|
VisualTransformation.None
|
||||||
|
} else {
|
||||||
|
PasswordVisualTransformation()
|
||||||
|
},
|
||||||
|
enabled = privateKeyEnabled,
|
||||||
|
onValueChange = { interfaceState = interfaceState.copy(privateKey = it) },
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(
|
||||||
|
enabled = privateKeyEnabled,
|
||||||
|
modifier = Modifier.focusRequester(FocusRequester.Default).size(iconSize),
|
||||||
|
onClick = {
|
||||||
|
val keypair = KeyPair()
|
||||||
|
interfaceState = interfaceState.copy(
|
||||||
|
privateKey = keypair.privateKey.toBase64(),
|
||||||
|
publicKey = keypair.publicKey.toBase64(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Refresh,
|
||||||
|
stringResource(R.string.rotate_keys),
|
||||||
|
tint = if (privateKeyEnabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.outline,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.private_key),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.base64_key),
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = MaterialTheme.colorScheme.outline,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
textStyle = MaterialTheme.typography.labelLarge,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.focusRequester(FocusRequester.Default),
|
||||||
|
value = interfaceState.publicKey,
|
||||||
|
enabled = false,
|
||||||
|
onValueChange = {
|
||||||
|
interfaceState = interfaceState.copy(publicKey = it)
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.focusRequester(FocusRequester.Default).size(iconSize),
|
||||||
|
onClick = {
|
||||||
|
clipboardManager.setText(
|
||||||
|
AnnotatedString(interfaceState.publicKey),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.ContentCopy,
|
||||||
|
stringResource(R.string.copy_public_key),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.public_key),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.base64_key),
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = MaterialTheme.colorScheme.outline,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.addresses,
|
||||||
|
onValueChange = {
|
||||||
|
interfaceState = interfaceState.copy(addresses = it)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.addresses),
|
||||||
|
hint = stringResource(R.string.comma_separated_list),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(end = 5.dp),
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.listenPort,
|
||||||
|
onValueChange = {
|
||||||
|
interfaceState = interfaceState.copy(listenPort = it)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.listen_port),
|
||||||
|
hint = stringResource(R.string.random),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||||
|
) {
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.dnsServers,
|
||||||
|
onValueChange = {
|
||||||
|
interfaceState = interfaceState.copy(dnsServers = it)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.dns_servers),
|
||||||
|
hint = stringResource(R.string.comma_separated_list),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(3 / 5f)
|
||||||
|
.padding(end = 5.dp),
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.mtu,
|
||||||
|
onValueChange = {
|
||||||
|
interfaceState = interfaceState.copy(mtu = it)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.mtu),
|
||||||
|
hint = stringResource(R.string.auto),
|
||||||
|
modifier = Modifier.width(IntrinsicSize.Min),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (showScripts) {
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.preUp,
|
||||||
|
onValueChange = {
|
||||||
|
interfaceState = interfaceState.copy(preUp = it)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.pre_up),
|
||||||
|
hint = stringResource(R.string.comma_separated_list).lowercase(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.postUp,
|
||||||
|
onValueChange = {
|
||||||
|
interfaceState = interfaceState.copy(postUp = it)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.post_up),
|
||||||
|
hint = stringResource(R.string.comma_separated_list).lowercase(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.preDown,
|
||||||
|
onValueChange = {
|
||||||
|
interfaceState = interfaceState.copy(preDown = it)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.pre_down),
|
||||||
|
hint = stringResource(R.string.comma_separated_list).lowercase(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.postDown,
|
||||||
|
onValueChange = {
|
||||||
|
interfaceState = interfaceState.copy(postDown = it)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.post_down),
|
||||||
|
hint = stringResource(R.string.comma_separated_list).lowercase(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (showAmneziaValues) {
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.junkPacketCount,
|
||||||
|
onValueChange = {
|
||||||
|
interfaceState = interfaceState.copy(junkPacketCount = it)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.junk_packet_count),
|
||||||
|
hint = stringResource(R.string.junk_packet_count).lowercase(),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.junkPacketMinSize,
|
||||||
|
onValueChange = {
|
||||||
|
interfaceState = interfaceState.copy(junkPacketMinSize = it)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.junk_packet_minimum_size),
|
||||||
|
hint =
|
||||||
|
stringResource(
|
||||||
|
R.string.junk_packet_minimum_size,
|
||||||
|
).lowercase(),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.junkPacketMaxSize,
|
||||||
|
onValueChange = {
|
||||||
|
interfaceState = interfaceState.copy(junkPacketMaxSize = it)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.junk_packet_maximum_size),
|
||||||
|
hint =
|
||||||
|
stringResource(
|
||||||
|
R.string.junk_packet_maximum_size,
|
||||||
|
).lowercase(),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.initPacketJunkSize,
|
||||||
|
onValueChange = {
|
||||||
|
interfaceState = interfaceState.copy(initPacketJunkSize = it)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.init_packet_junk_size),
|
||||||
|
hint = stringResource(R.string.init_packet_junk_size).lowercase(),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.responsePacketJunkSize,
|
||||||
|
onValueChange = {
|
||||||
|
interfaceState = interfaceState.copy(responsePacketJunkSize = it)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.response_packet_junk_size),
|
||||||
|
hint =
|
||||||
|
stringResource(
|
||||||
|
R.string.response_packet_junk_size,
|
||||||
|
).lowercase(),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.initPacketMagicHeader,
|
||||||
|
onValueChange = {
|
||||||
|
interfaceState = interfaceState.copy(initPacketMagicHeader = it)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.init_packet_magic_header),
|
||||||
|
hint =
|
||||||
|
stringResource(
|
||||||
|
R.string.init_packet_magic_header,
|
||||||
|
).lowercase(),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.responsePacketMagicHeader,
|
||||||
|
onValueChange = {
|
||||||
|
interfaceState = interfaceState.copy(responsePacketMagicHeader = it)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.response_packet_magic_header),
|
||||||
|
hint =
|
||||||
|
stringResource(
|
||||||
|
R.string.response_packet_magic_header,
|
||||||
|
).lowercase(),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.underloadPacketMagicHeader,
|
||||||
|
onValueChange = {
|
||||||
|
interfaceState = interfaceState.copy(underloadPacketMagicHeader = it)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.underload_packet_magic_header),
|
||||||
|
hint =
|
||||||
|
stringResource(
|
||||||
|
R.string.underload_packet_magic_header,
|
||||||
|
).lowercase(),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.transportPacketMagicHeader,
|
||||||
|
onValueChange = {
|
||||||
|
interfaceState = interfaceState.copy(transportPacketMagicHeader = it)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.transport_packet_magic_header),
|
||||||
|
hint =
|
||||||
|
stringResource(
|
||||||
|
R.string.transport_packet_magic_header,
|
||||||
|
).lowercase(),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
peersState.forEachIndexed { index, peer ->
|
||||||
|
var isPeerDropDownExpanded by remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
val isLanExcluded = peer.isLanExcluded()
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp, Alignment.Top),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp.scaledWidth())
|
||||||
|
.focusGroup(),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier =
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
GroupLabel(
|
||||||
|
stringResource(R.string.peer),
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.End),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.size(iconSize),
|
||||||
|
onClick = {
|
||||||
|
// TODO make a dialog to confirm this
|
||||||
|
peersState.removeAt(index)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
val icon = Icons.Rounded.Delete
|
||||||
|
Icon(icon, icon.name)
|
||||||
|
}
|
||||||
|
Column {
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.size(iconSize),
|
||||||
|
onClick = {
|
||||||
|
isPeerDropDownExpanded = true
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
val icon = Icons.Rounded.MoreVert
|
||||||
|
Icon(icon, icon.name)
|
||||||
|
}
|
||||||
|
DropdownMenu(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
expanded = isPeerDropDownExpanded,
|
||||||
|
modifier = Modifier.shadow(12.dp).background(MaterialTheme.colorScheme.surface),
|
||||||
|
onDismissRequest = {
|
||||||
|
isPeerDropDownExpanded = false
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
PeerActions.entries.forEach { action ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = when (action) {
|
||||||
|
PeerActions.EXCLUDE_LAN -> if (isLanExcluded) {
|
||||||
|
stringResource(R.string.include_lan)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.exclude_lan)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
isPeerDropDownExpanded = false
|
||||||
|
when (action) {
|
||||||
|
PeerActions.EXCLUDE_LAN -> if (isLanExcluded) {
|
||||||
|
peersState[index] = peer.includeLan()
|
||||||
|
} else {
|
||||||
|
peersState[index] = peer.excludeLan()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = peer.publicKey,
|
||||||
|
onValueChange = { value ->
|
||||||
|
peersState[index] = peersState[index].copy(publicKey = value)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.public_key),
|
||||||
|
hint = stringResource(R.string.base64_key),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
val presharedKeyEnabled = (tunnelConf == null) || isAuthenticated ||
|
||||||
|
with(configPair.second?.peers[index]?.preSharedKey) { this?.isEmpty == true || this?.isPresent == false }
|
||||||
|
OutlinedTextField(
|
||||||
|
textStyle = MaterialTheme.typography.labelLarge,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { showAuthPrompt = true },
|
||||||
|
value = peer.preSharedKey,
|
||||||
|
visualTransformation =
|
||||||
|
if (presharedKeyEnabled) {
|
||||||
|
VisualTransformation.None
|
||||||
|
} else {
|
||||||
|
PasswordVisualTransformation()
|
||||||
|
},
|
||||||
|
enabled = presharedKeyEnabled,
|
||||||
|
onValueChange = { value -> peersState[index] = peersState[index].copy(preSharedKey = value) },
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.preshared_key),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.optional),
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = MaterialTheme.colorScheme.outline,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
textStyle = MaterialTheme.typography.labelLarge,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = peer.persistentKeepalive,
|
||||||
|
enabled = true,
|
||||||
|
onValueChange = { value ->
|
||||||
|
peersState[index] = peersState[index].copy(persistentKeepalive = value)
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.seconds),
|
||||||
|
modifier = Modifier.padding(end = 10.dp),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = {
|
||||||
|
Text(stringResource(R.string.persistent_keepalive), color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.labelMedium)
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
placeholder = {
|
||||||
|
Text(stringResource(R.string.optional_no_recommend), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.outline)
|
||||||
|
},
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = peer.endpoint,
|
||||||
|
onValueChange = { value ->
|
||||||
|
peersState[index] = peersState[index].copy(endpoint = value)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.endpoint),
|
||||||
|
hint = stringResource(R.string.endpoint).lowercase(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
textStyle = MaterialTheme.typography.labelLarge,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = peer.allowedIps,
|
||||||
|
enabled = true,
|
||||||
|
onValueChange = { value ->
|
||||||
|
peersState[index] = peersState[index].copy(allowedIps = value)
|
||||||
|
},
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.allowed_ips),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
placeholder = {
|
||||||
|
Text(stringResource(R.string.comma_separated_list), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.outline)
|
||||||
|
},
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(bottom = 140.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
TextButton(onClick = {
|
||||||
|
peersState.add(PeerProxy())
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.add_peer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+198
-57
@@ -2,63 +2,128 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
|||||||
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
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.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.overscroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.outlined.Add
|
||||||
|
import androidx.compose.material3.FabPosition
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.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.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.NestedScrollListener
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult
|
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.permission.vpn.withVpnPermission
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.permission.withIgnoreBatteryOpt
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.AutoTunnelRowItem
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.GettingStartedLabel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ScrollDismissFab
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelImportSheet
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelImportSheet
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelList
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelRowItem
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.UrlImportDialog
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppViewState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
|
import com.zaneschepke.wireguardautotunnel.viewmodel.MainViewModel
|
||||||
|
import java.text.Collator
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(appUiState: AppUiState, appViewState: AppViewState, viewModel: AppViewModel) {
|
fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState) {
|
||||||
|
val context = LocalContext.current
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val clipboard = LocalClipboardManager.current
|
val clipboard = LocalClipboardManager.current
|
||||||
|
val snackbar = SnackbarController.current
|
||||||
|
|
||||||
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
|
var isFabVisible by rememberSaveable { mutableStateOf(true) }
|
||||||
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
|
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
|
||||||
var showUrlImportDialog by remember { mutableStateOf(false) }
|
var selectedTunnel by remember { mutableStateOf<TunnelConf?>(null) }
|
||||||
|
val isRunningOnTv = remember { context.isRunningOnTv() }
|
||||||
|
|
||||||
val tunnelFileImportResultLauncher = rememberFileImportLauncherForResult(
|
val activeTunnels by viewModel.activeTunnels.collectAsStateWithLifecycle(emptyList())
|
||||||
onNoFileExplorer = { viewModel.handleEvent(AppEvent.ShowMessage(StringValue.StringResource(R.string.error_no_file_explorer))) },
|
|
||||||
onData = { data -> viewModel.handleEvent(AppEvent.ImportTunnelFromFile(data)) },
|
|
||||||
)
|
|
||||||
|
|
||||||
val requestPermissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
val collator = Collator.getInstance(Locale.getDefault())
|
||||||
if (!isGranted) {
|
|
||||||
viewModel.handleEvent(
|
val sortedTunnels = remember(uiState.tunnels) {
|
||||||
AppEvent.ShowMessage(StringValue.StringResource(R.string.camera_permission_required)),
|
uiState.tunnels.sortedWith(compareBy(collator) { it.tunName })
|
||||||
)
|
|
||||||
return@rememberLauncherForActivityResult
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val startAutoTunnel = withVpnPermission<Unit> { viewModel.onToggleAutoTunnel() }
|
||||||
|
val startTunnel = withVpnPermission<TunnelConf> {
|
||||||
|
viewModel.onTunnelStart(it)
|
||||||
|
}
|
||||||
|
val autoTunnelToggleBattery = withIgnoreBatteryOpt(uiState.generalState.isBatteryOptimizationDisableShown) {
|
||||||
|
if (!uiState.generalState.isBatteryOptimizationDisableShown) viewModel.setBatteryOptimizeDisableShown()
|
||||||
|
if (uiState.appSettings.isKernelEnabled) {
|
||||||
|
viewModel.onToggleAutoTunnel()
|
||||||
|
} else {
|
||||||
|
startAutoTunnel.invoke(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val nestedScrollConnection = remember {
|
||||||
|
NestedScrollListener({ isFabVisible = false }, { isFabVisible = true })
|
||||||
|
}
|
||||||
|
|
||||||
|
val tunnelFileImportResultLauncher = rememberFileImportLauncherForResult(onNoFileExplorer = {
|
||||||
|
snackbar.showMessage(
|
||||||
|
context.getString(R.string.error_no_file_explorer),
|
||||||
|
)
|
||||||
|
}, onData = { data ->
|
||||||
|
viewModel.onTunnelFileSelected(data, context)
|
||||||
|
})
|
||||||
|
|
||||||
|
val requestPermissionLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission(),
|
||||||
|
) { isGranted ->
|
||||||
|
if (!isGranted) return@rememberLauncherForActivityResult snackbar.showMessage("Camera permission required")
|
||||||
navController.navigate(Route.Scanner)
|
navController.navigate(Route.Scanner)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showDeleteTunnelAlertDialog && appViewState.selectedTunnel != null) {
|
if (showDeleteTunnelAlertDialog) {
|
||||||
InfoDialog(
|
InfoDialog(
|
||||||
onDismiss = { showDeleteTunnelAlertDialog = false },
|
onDismiss = { showDeleteTunnelAlertDialog = false },
|
||||||
onAttest = {
|
onAttest = {
|
||||||
appViewState.selectedTunnel.let { viewModel.handleEvent(AppEvent.DeleteTunnel(it)) }
|
selectedTunnel?.let { viewModel.onDelete(it) }
|
||||||
showDeleteTunnelAlertDialog = false
|
showDeleteTunnelAlertDialog = false
|
||||||
viewModel.handleEvent(AppEvent.SetSelectedTunnel(null))
|
selectedTunnel = null
|
||||||
},
|
},
|
||||||
title = { Text(text = stringResource(R.string.delete_tunnel)) },
|
title = { Text(text = stringResource(R.string.delete_tunnel)) },
|
||||||
body = { Text(text = stringResource(R.string.delete_tunnel_message)) },
|
body = { Text(text = stringResource(R.string.delete_tunnel_message)) },
|
||||||
@@ -66,46 +131,122 @@ fun MainScreen(appUiState: AppUiState, appViewState: AppViewState, viewModel: Ap
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
TunnelImportSheet(
|
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConf) {
|
||||||
appViewState.showBottomSheet,
|
if (!checked) {
|
||||||
onDismiss = { viewModel.handleEvent(AppEvent.ToggleBottomSheet) },
|
viewModel.onTunnelStop(tunnel)
|
||||||
onFileClick = { tunnelFileImportResultLauncher.launch(Constants.ALLOWED_TV_FILE_TYPES) },
|
return
|
||||||
onQrClick = { requestPermissionLauncher.launch(android.Manifest.permission.CAMERA) },
|
}
|
||||||
onClipboardClick = { clipboard.getText()?.text?.let { viewModel.handleEvent(AppEvent.ImportTunnelFromClipboard(it)) } },
|
if (uiState.appSettings.isKernelEnabled) {
|
||||||
onManualImportClick = { navController.navigate(Route.Config(Constants.MANUAL_TUNNEL_CONFIG_ID)) },
|
viewModel.onTunnelStart(tunnel)
|
||||||
onUrlClick = { showUrlImportDialog = true },
|
} else {
|
||||||
)
|
startTunnel.invoke(tunnel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (showUrlImportDialog) {
|
Scaffold(
|
||||||
UrlImportDialog(
|
modifier =
|
||||||
onDismiss = { showUrlImportDialog = false },
|
Modifier.pointerInput(Unit) {
|
||||||
onConfirm = { url ->
|
if (uiState.tunnels.isEmpty()) return@pointerInput
|
||||||
viewModel.handleEvent(AppEvent.ImportTunnelFromUrl(url))
|
detectTapGestures(
|
||||||
showUrlImportDialog = false
|
onTap = {
|
||||||
|
selectedTunnel = null
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
floatingActionButtonPosition = FabPosition.End,
|
||||||
|
floatingActionButton = {
|
||||||
|
if (!isRunningOnTv) {
|
||||||
|
ScrollDismissFab({
|
||||||
|
val icon = Icons.Filled.Add
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = icon.name,
|
||||||
|
tint = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
)
|
||||||
|
}, isVisible = isFabVisible, onClick = {
|
||||||
|
showBottomSheet = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
topBar = {
|
||||||
|
if (isRunningOnTv) {
|
||||||
|
TopNavBar(
|
||||||
|
showBack = false,
|
||||||
|
title = stringResource(R.string.app_name),
|
||||||
|
trailing = {
|
||||||
|
IconButton(onClick = {
|
||||||
|
showBottomSheet = true
|
||||||
|
}) {
|
||||||
|
val icon = Icons.Outlined.Add
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = icon.name,
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
TunnelList(
|
|
||||||
appUiState = appUiState,
|
|
||||||
activeTunnels = appUiState.activeTunnels,
|
|
||||||
selectedTunnel = appViewState.selectedTunnel,
|
|
||||||
onSetSelectedTunnel = { viewModel.handleEvent(AppEvent.SetSelectedTunnel(it)) },
|
|
||||||
onDeleteTunnel = {
|
|
||||||
viewModel.handleEvent(AppEvent.SetSelectedTunnel(it))
|
|
||||||
showDeleteTunnelAlertDialog = true
|
|
||||||
},
|
},
|
||||||
onToggleTunnel = { tunnel, checked ->
|
) { padding ->
|
||||||
if (checked) viewModel.handleEvent(AppEvent.StartTunnel(tunnel)) else viewModel.handleEvent(AppEvent.StopTunnel(tunnel))
|
TunnelImportSheet(
|
||||||
|
showBottomSheet,
|
||||||
|
onDismiss = { showBottomSheet = false },
|
||||||
|
onFileClick = { tunnelFileImportResultLauncher.launch(Constants.ALLOWED_TV_FILE_TYPES) },
|
||||||
|
onQrClick = { requestPermissionLauncher.launch(android.Manifest.permission.CAMERA) },
|
||||||
|
onClipboardClick = {
|
||||||
|
clipboard.getText()?.text?.let {
|
||||||
|
viewModel.onClipboardImport(it)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onExpandStats = { viewModel.handleEvent(AppEvent.ToggleTunnelStatsExpanded) },
|
onManualImportClick = {
|
||||||
onCopyTunnel = {
|
navController.navigate(
|
||||||
viewModel.handleEvent(AppEvent.CopyTunnel(it))
|
Route.Config(Constants.MANUAL_TUNNEL_CONFIG_ID),
|
||||||
viewModel.handleEvent(AppEvent.SetSelectedTunnel(null))
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(top = 12.dp, bottom = 24.dp).padding(horizontal = 12.dp),
|
|
||||||
viewModel = viewModel,
|
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
LazyColumn(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(5.dp, Alignment.Top),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize().padding(padding).padding(top = 24.dp.scaledHeight())
|
||||||
|
.overscroll(ScrollableDefaults.overscrollEffect())
|
||||||
|
.nestedScroll(nestedScrollConnection),
|
||||||
|
state = rememberLazyListState(0, uiState.tunnels.count()),
|
||||||
|
userScrollEnabled = true,
|
||||||
|
reverseLayout = false,
|
||||||
|
flingBehavior = ScrollableDefaults.flingBehavior(),
|
||||||
|
) {
|
||||||
|
if (uiState.tunnels.isEmpty()) {
|
||||||
|
item {
|
||||||
|
GettingStartedLabel(onClick = { context.openWebUrl(it) })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item {
|
||||||
|
AutoTunnelRowItem(uiState) {
|
||||||
|
autoTunnelToggleBattery.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items(
|
||||||
|
sortedTunnels,
|
||||||
|
key = { tunnel -> tunnel.id },
|
||||||
|
) { tunnel ->
|
||||||
|
val expanded = uiState.generalState.isTunnelStatsExpanded
|
||||||
|
val tunnelState = activeTunnels.firstOrNull { it.id == tunnel.id }?.state?.collectAsStateWithLifecycle()
|
||||||
|
TunnelRowItem(
|
||||||
|
tunnel.isActive,
|
||||||
|
expanded,
|
||||||
|
selectedTunnel?.id == tunnel.id,
|
||||||
|
tunnel,
|
||||||
|
tunnelState = tunnelState?.value ?: TunnelState(),
|
||||||
|
{ selectedTunnel = tunnel },
|
||||||
|
{ viewModel.onExpandedChanged(!expanded) },
|
||||||
|
onDelete = { showDeleteTunnelAlertDialog = true },
|
||||||
|
onCopy = { viewModel.onCopyTunnel(tunnel) },
|
||||||
|
onSwitchClick = { onTunnelToggle(it, tunnel) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-8
@@ -1,4 +1,4 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.pin
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||||
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -6,18 +6,19 @@ import androidx.compose.runtime.Composable
|
|||||||
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 com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
|
||||||
import xyz.teamgravity.pin_lock_compose.PinLock
|
import xyz.teamgravity.pin_lock_compose.PinLock
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PinLockScreen(viewModel: AppViewModel) {
|
fun PinLockScreen(appViewModel: AppViewModel) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
|
val snackbar = SnackbarController.current
|
||||||
PinLock(
|
PinLock(
|
||||||
title = { pinExists ->
|
title = { pinExists ->
|
||||||
Text(
|
Text(
|
||||||
@@ -47,13 +48,16 @@ fun PinLockScreen(viewModel: AppViewModel) {
|
|||||||
},
|
},
|
||||||
onPinIncorrect = {
|
onPinIncorrect = {
|
||||||
// pin is incorrect, show error
|
// pin is incorrect, show error
|
||||||
viewModel.handleEvent(AppEvent.ShowMessage(StringValue.StringResource(R.string.incorrect_pin)))
|
snackbar.showMessage(
|
||||||
|
StringValue.StringResource(R.string.incorrect_pin).asString(context),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onPinCreated = {
|
onPinCreated = {
|
||||||
// pin created for the first time, navigate or hide pin lock
|
// pin created for the first time, navigate or hide pin lock
|
||||||
viewModel.handleEvent(AppEvent.ShowMessage(StringValue.StringResource(R.string.pin_created)))
|
snackbar.showMessage(
|
||||||
|
StringValue.StringResource(R.string.pin_created).asString(context),
|
||||||
viewModel.handleEvent(AppEvent.TogglePinLock)
|
)
|
||||||
|
appViewModel.onPinLockEnabled()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+15
-5
@@ -1,18 +1,28 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.scanner
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.journeyapps.barcodescanner.CompoundBarcodeView
|
import com.journeyapps.barcodescanner.CompoundBarcodeView
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
import com.zaneschepke.wireguardautotunnel.viewmodel.ScannerViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ScannerScreen(viewModel: AppViewModel) {
|
fun ScannerScreen(viewModel: ScannerViewModel = hiltViewModel()) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
|
||||||
|
val success = viewModel.success.collectAsStateWithLifecycle(null)
|
||||||
|
|
||||||
|
LaunchedEffect(success.value) {
|
||||||
|
if (success.value != null) navController.popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
val barcodeView = remember {
|
val barcodeView = remember {
|
||||||
CompoundBarcodeView(context).apply {
|
CompoundBarcodeView(context).apply {
|
||||||
@@ -20,7 +30,7 @@ fun ScannerScreen(viewModel: AppViewModel) {
|
|||||||
this.setStatusText("")
|
this.setStatusText("")
|
||||||
this.decodeSingle { result ->
|
this.decodeSingle { result ->
|
||||||
result.text?.let { barCodeOrQr ->
|
result.text?.let { barCodeOrQr ->
|
||||||
viewModel.handleEvent(AppEvent.ImportTunnelFromQrCode(barCodeOrQr))
|
viewModel.onTunnelQrResult(barCodeOrQr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+269
@@ -0,0 +1,269 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Check
|
||||||
|
import androidx.compose.material.icons.outlined.Save
|
||||||
|
import androidx.compose.material.icons.outlined.Search
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.MultiChoiceSegmentedButtonRow
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SegmentedButton
|
||||||
|
import androidx.compose.material3.SegmentedButtonDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.toMutableStateList
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
|
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.SelectionItemButton
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.enums.SplitOptions
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||||
|
import java.text.Collator
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SplitTunnelScreen(tunnelConf: TunnelConf, viewModel: AppViewModel) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
|
||||||
|
val inputHeight = 45.dp
|
||||||
|
|
||||||
|
val collator = Collator.getInstance(Locale.getDefault())
|
||||||
|
|
||||||
|
val popBackStack by viewModel.popBackStack.collectAsStateWithLifecycle(false)
|
||||||
|
|
||||||
|
LaunchedEffect(popBackStack) {
|
||||||
|
if (popBackStack) navController.popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
val splitTunnelApps by viewModel.splitTunnelApps.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
var proxyInterface by remember { mutableStateOf(InterfaceProxy()) }
|
||||||
|
|
||||||
|
var selectedSplitOption by remember { mutableStateOf(SplitOptions.ALL) }
|
||||||
|
|
||||||
|
val selectedPackages = remember { mutableStateListOf<String>() }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
proxyInterface = InterfaceProxy.from(tunnelConf.toAmConfig().`interface`)
|
||||||
|
val pair = when {
|
||||||
|
proxyInterface.excludedApplications.isNotEmpty() -> Pair(SplitOptions.EXCLUDE, proxyInterface.excludedApplications)
|
||||||
|
proxyInterface.includedApplications.isNotEmpty() -> Pair(SplitOptions.INCLUDE, proxyInterface.includedApplications)
|
||||||
|
else -> Pair(SplitOptions.ALL, mutableSetOf())
|
||||||
|
}
|
||||||
|
selectedSplitOption = pair.first
|
||||||
|
selectedPackages.addAll(pair.second)
|
||||||
|
}
|
||||||
|
|
||||||
|
var query: String by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
val sortedPackages by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
splitTunnelApps.sortedWith(compareBy(collator) { it.name }).filter { it.name.lowercase().contains(query.lowercase()) }.toMutableStateList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
// clean up any split tunnel packages for apps that were uninstalled
|
||||||
|
viewModel.cleanUpUninstalledApps(tunnelConf, splitTunnelApps.map { it.`package` })
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopNavBar(stringResource(R.string.tunneling_apps), trailing = {
|
||||||
|
IconButton(onClick = {
|
||||||
|
proxyInterface.apply {
|
||||||
|
includedApplications.clear()
|
||||||
|
excludedApplications.clear()
|
||||||
|
}
|
||||||
|
when (selectedSplitOption) {
|
||||||
|
SplitOptions.INCLUDE -> proxyInterface.includedApplications.apply {
|
||||||
|
addAll(selectedPackages)
|
||||||
|
}
|
||||||
|
SplitOptions.EXCLUDE -> proxyInterface.excludedApplications.apply {
|
||||||
|
addAll(selectedPackages)
|
||||||
|
}
|
||||||
|
SplitOptions.ALL -> Unit
|
||||||
|
}
|
||||||
|
viewModel.updateExistingTunnelConfig(tunnelConf, `interface` = proxyInterface)
|
||||||
|
}) {
|
||||||
|
val icon = Icons.Outlined.Save
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = icon.name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
) { padding ->
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.CenterVertically),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(padding)
|
||||||
|
.padding(top = 24.dp.scaledHeight()),
|
||||||
|
) {
|
||||||
|
MultiChoiceSegmentedButtonRow(
|
||||||
|
modifier = Modifier.background(color = MaterialTheme.colorScheme.background).fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp.scaledWidth()).height(inputHeight),
|
||||||
|
) {
|
||||||
|
SplitOptions.entries.forEachIndexed { index, entry ->
|
||||||
|
val active = selectedSplitOption == entry
|
||||||
|
SegmentedButton(
|
||||||
|
shape = SegmentedButtonDefaults.itemShape(index = index, count = SplitOptions.entries.size, baseShape = RoundedCornerShape(8.dp)),
|
||||||
|
icon = {
|
||||||
|
SegmentedButtonDefaults.Icon(active = active, activeContent = {
|
||||||
|
val icon = Icons.Outlined.Check
|
||||||
|
Icon(imageVector = icon, icon.name, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(SegmentedButtonDefaults.IconSize))
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
imageVector = entry.icon(),
|
||||||
|
contentDescription = entry.icon().name,
|
||||||
|
modifier = Modifier.size(SegmentedButtonDefaults.IconSize),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = SegmentedButtonDefaults.colors().copy(
|
||||||
|
activeContainerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
inactiveContainerColor = MaterialTheme.colorScheme.background,
|
||||||
|
),
|
||||||
|
onCheckedChange = {
|
||||||
|
selectedSplitOption = entry
|
||||||
|
},
|
||||||
|
checked = active,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
entry.text().asString(context)
|
||||||
|
.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() },
|
||||||
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (selectedSplitOption != SplitOptions.ALL) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
CustomTextField(
|
||||||
|
textStyle = MaterialTheme.typography.labelMedium.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
|
),
|
||||||
|
value = query,
|
||||||
|
onValueChange = { input ->
|
||||||
|
query = input
|
||||||
|
},
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
label = {},
|
||||||
|
leading = {
|
||||||
|
val icon = Icons.Outlined.Search
|
||||||
|
Icon(icon, icon.name)
|
||||||
|
},
|
||||||
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth().height(inputHeight).padding(horizontal = 24.dp.scaledWidth()),
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions(
|
||||||
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
imeAction = ImeAction.Done,
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(),
|
||||||
|
)
|
||||||
|
LazyColumn(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
contentPadding = PaddingValues(top = 10.dp),
|
||||||
|
) {
|
||||||
|
items(sortedPackages, key = { it.`package` }) { app ->
|
||||||
|
val checked = selectedPackages.contains(app.`package`)
|
||||||
|
val onClick = {
|
||||||
|
if (checked) selectedPackages.remove(app.`package`) else selectedPackages.add(app.`package`)
|
||||||
|
}
|
||||||
|
SelectionItemButton(
|
||||||
|
{
|
||||||
|
Image(
|
||||||
|
rememberDrawablePainter(app.icon),
|
||||||
|
app.name,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(horizontal = 24.dp.scaledWidth())
|
||||||
|
.size(
|
||||||
|
iconSize,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
buttonText = app.name,
|
||||||
|
onClick = {
|
||||||
|
onClick()
|
||||||
|
},
|
||||||
|
trailing = {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = {
|
||||||
|
onClick()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+175
@@ -0,0 +1,175 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.PhoneAndroid
|
||||||
|
import androidx.compose.material.icons.outlined.Security
|
||||||
|
import androidx.compose.material.icons.outlined.SettingsEthernet
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
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.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.TrustedNetworkTextBox
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.WildcardsLabel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||||
|
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelAutoTunnelViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TunnelAutoTunnelScreen(tunnelConf: TunnelConf, appSettings: AppSettings, tunnelAutoTunnelViewModel: TunnelAutoTunnelViewModel = hiltViewModel()) {
|
||||||
|
var currentText by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
LaunchedEffect(tunnelConf.tunnelNetworks) {
|
||||||
|
currentText = ""
|
||||||
|
}
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopNavBar(tunnelConf.tunName)
|
||||||
|
},
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(top = 24.dp.scaledHeight())
|
||||||
|
.padding(horizontal = 24.dp.scaledWidth()),
|
||||||
|
) {
|
||||||
|
SurfaceSelectionGroupButton(
|
||||||
|
buildList {
|
||||||
|
addAll(
|
||||||
|
listOf(
|
||||||
|
SelectionItem(
|
||||||
|
Icons.Outlined.PhoneAndroid,
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.mobile_tunnel),
|
||||||
|
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
description = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.mobile_data_tunnel),
|
||||||
|
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailing = {
|
||||||
|
ScaledSwitch(
|
||||||
|
tunnelConf.isMobileDataTunnel,
|
||||||
|
onClick = { tunnelAutoTunnelViewModel.onToggleIsMobileDataTunnel(tunnelConf) },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = { tunnelAutoTunnelViewModel.onToggleIsMobileDataTunnel(tunnelConf) },
|
||||||
|
),
|
||||||
|
SelectionItem(
|
||||||
|
Icons.Outlined.SettingsEthernet,
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.ethernet_tunnel),
|
||||||
|
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
description = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.set_ethernet_tunnel),
|
||||||
|
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailing = {
|
||||||
|
ScaledSwitch(
|
||||||
|
tunnelConf.isEthernetTunnel,
|
||||||
|
onClick = { tunnelAutoTunnelViewModel.onToggleIsEthernetTunnel(tunnelConf) },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = { tunnelAutoTunnelViewModel.onToggleIsEthernetTunnel(tunnelConf) },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
add(
|
||||||
|
SelectionItem(
|
||||||
|
title = {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 4.dp.scaledHeight()),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(4f, false)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
val icon = Icons.Outlined.Security
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
icon.name,
|
||||||
|
modifier = Modifier.size(iconSize),
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 16.dp.scaledWidth())
|
||||||
|
.padding(vertical = 6.dp.scaledHeight()),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.use_tunnel_on_wifi_name),
|
||||||
|
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
description = {
|
||||||
|
TrustedNetworkTextBox(
|
||||||
|
tunnelConf.tunnelNetworks,
|
||||||
|
onDelete = { tunnelAutoTunnelViewModel.onDeleteRunSSID(it, tunnelConf) },
|
||||||
|
currentText = currentText,
|
||||||
|
onSave = { tunnelAutoTunnelViewModel.onSaveRunSSID(it, tunnelConf) },
|
||||||
|
onValueChange = { currentText = it },
|
||||||
|
supporting = {
|
||||||
|
if (appSettings.isWildcardsEnabled) {
|
||||||
|
WildcardsLabel()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+253
@@ -0,0 +1,253 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.outlined.CallSplit
|
||||||
|
import androidx.compose.material.icons.outlined.Bolt
|
||||||
|
import androidx.compose.material.icons.outlined.Dns
|
||||||
|
import androidx.compose.material.icons.outlined.Edit
|
||||||
|
import androidx.compose.material.icons.outlined.NetworkPing
|
||||||
|
import androidx.compose.material.icons.outlined.Star
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
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.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationTextBox
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||||
|
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelOptionsViewModel
|
||||||
|
import kotlin.text.isBlank
|
||||||
|
import kotlin.text.isNullOrBlank
|
||||||
|
import kotlin.text.toLong
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OptionsScreen(tunnelConf: TunnelConf, viewModel: TunnelOptionsViewModel = hiltViewModel()) {
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
|
||||||
|
var currentText by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
LaunchedEffect(tunnelConf.tunnelNetworks) {
|
||||||
|
currentText = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val onPingToggle = {
|
||||||
|
viewModel.saveTunnel(tunnelConf.copy(isPingEnabled = !tunnelConf.isPingEnabled))
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopNavBar(tunnelConf.tunName)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(it)
|
||||||
|
.imePadding()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(top = 24.dp.scaledHeight())
|
||||||
|
.padding(horizontal = 24.dp.scaledWidth()),
|
||||||
|
) {
|
||||||
|
SurfaceSelectionGroupButton(
|
||||||
|
listOf(
|
||||||
|
SelectionItem(
|
||||||
|
Icons.Outlined.Star,
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.primary_tunnel),
|
||||||
|
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
description = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.set_primary_tunnel),
|
||||||
|
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailing = {
|
||||||
|
ScaledSwitch(
|
||||||
|
tunnelConf.isPrimaryTunnel,
|
||||||
|
onClick = { viewModel.onTogglePrimaryTunnel(tunnelConf) },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = { viewModel.onTogglePrimaryTunnel(tunnelConf) },
|
||||||
|
),
|
||||||
|
SelectionItem(
|
||||||
|
Icons.Outlined.Bolt,
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.auto_tunneling),
|
||||||
|
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
description = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.tunnel_specific_settings),
|
||||||
|
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
navController.navigate(Route.TunnelAutoTunnel(id = tunnelConf.id))
|
||||||
|
},
|
||||||
|
trailing = {
|
||||||
|
ForwardButton { navController.navigate(Route.TunnelAutoTunnel(id = tunnelConf.id)) }
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SelectionItem(
|
||||||
|
Icons.Outlined.Edit,
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.edit_tunnel),
|
||||||
|
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
navController.navigate(Route.Config(id = tunnelConf.id))
|
||||||
|
},
|
||||||
|
trailing = {
|
||||||
|
ForwardButton { navController.navigate(Route.Config(id = tunnelConf.id)) }
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SelectionItem(
|
||||||
|
Icons.Outlined.Dns,
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.server_ipv4),
|
||||||
|
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
description = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.prefer_ipv4),
|
||||||
|
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailing = {
|
||||||
|
ScaledSwitch(
|
||||||
|
tunnelConf.isIpv4Preferred,
|
||||||
|
onClick = { viewModel.onToggleIpv4(tunnelConf) },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = { viewModel.onToggleIpv4(tunnelConf) },
|
||||||
|
),
|
||||||
|
SelectionItem(
|
||||||
|
Icons.AutoMirrored.Outlined.CallSplit,
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.splt_tunneling),
|
||||||
|
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
navController.navigate(Route.SplitTunnel(id = tunnelConf.id))
|
||||||
|
},
|
||||||
|
trailing = {
|
||||||
|
ForwardButton { navController.navigate(Route.SplitTunnel(id = tunnelConf.id)) }
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
SurfaceSelectionGroupButton(
|
||||||
|
buildList {
|
||||||
|
add(
|
||||||
|
SelectionItem(
|
||||||
|
Icons.Outlined.NetworkPing,
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.restart_on_ping),
|
||||||
|
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailing = {
|
||||||
|
ScaledSwitch(
|
||||||
|
checked = tunnelConf.isPingEnabled,
|
||||||
|
onClick = { onPingToggle() },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = { onPingToggle() },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if (tunnelConf.isPingEnabled) {
|
||||||
|
add(
|
||||||
|
SelectionItem(
|
||||||
|
title = {},
|
||||||
|
description = {
|
||||||
|
SubmitConfigurationTextBox(
|
||||||
|
tunnelConf.pingIp,
|
||||||
|
stringResource(R.string.set_custom_ping_ip),
|
||||||
|
stringResource(R.string.default_ping_ip),
|
||||||
|
isErrorValue = { !it.isNullOrBlank() && !it.isValidIpv4orIpv6Address() },
|
||||||
|
onSubmit = {
|
||||||
|
viewModel.saveTunnel(
|
||||||
|
tunnelConf.copy(pingIp = it.ifBlank { null }),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
fun isSecondsError(seconds: String?): Boolean {
|
||||||
|
return seconds?.let { value -> if (value.isBlank()) false else value.toLong() >= Long.MAX_VALUE / 1000 } == true
|
||||||
|
}
|
||||||
|
SubmitConfigurationTextBox(
|
||||||
|
tunnelConf.pingInterval?.let { (it / 1000).toString() },
|
||||||
|
stringResource(R.string.set_custom_ping_internal),
|
||||||
|
"(${stringResource(R.string.optional_default)} ${Constants.PING_INTERVAL / 1000})",
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Number,
|
||||||
|
imeAction = ImeAction.Done,
|
||||||
|
),
|
||||||
|
isErrorValue = ::isSecondsError,
|
||||||
|
onSubmit = {
|
||||||
|
viewModel.onPingIntervalChange(tunnelConf, it)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
SubmitConfigurationTextBox(
|
||||||
|
tunnelConf.pingCooldown?.let { (it / 1000).toString() },
|
||||||
|
stringResource(R.string.set_custom_ping_cooldown),
|
||||||
|
"(${stringResource(R.string.optional_default)} ${Constants.PING_COOLDOWN / 1000})",
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Number,
|
||||||
|
),
|
||||||
|
isErrorValue = ::isSecondsError,
|
||||||
|
onSubmit = { viewModel.onPingCoolDownChange(tunnelConf, it) },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-51
@@ -1,51 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
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.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.components.EthernetTunnelItem
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.components.MobileDataTunnelItem
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.components.WifiTunnelItem
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun TunnelAutoTunnelScreen(tunnelConf: TunnelConf, appSettings: AppSettings, viewModel: AppViewModel) {
|
|
||||||
var currentText by remember { mutableStateOf("") }
|
|
||||||
|
|
||||||
LaunchedEffect(tunnelConf.tunnelNetworks) {
|
|
||||||
currentText = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.padding(top = 24.dp)
|
|
||||||
.padding(horizontal = 24.dp),
|
|
||||||
) {
|
|
||||||
SurfaceSelectionGroupButton(
|
|
||||||
items = buildList {
|
|
||||||
add(MobileDataTunnelItem(tunnelConf, viewModel))
|
|
||||||
add(EthernetTunnelItem(tunnelConf, viewModel))
|
|
||||||
add(WifiTunnelItem(tunnelConf, appSettings, viewModel, currentText) { currentText = it })
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-40
@@ -1,40 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.components
|
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.SettingsEthernet
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun EthernetTunnelItem(tunnelConf: TunnelConf, viewModel: AppViewModel): SelectionItem {
|
|
||||||
return SelectionItem(
|
|
||||||
leadingIcon = Icons.Outlined.SettingsEthernet,
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.ethernet_tunnel),
|
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
description = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.set_ethernet_tunnel),
|
|
||||||
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
trailing = {
|
|
||||||
ScaledSwitch(
|
|
||||||
checked = tunnelConf.isEthernetTunnel,
|
|
||||||
onClick = { viewModel.handleEvent(AppEvent.ToggleEthernetTunnel(tunnelConf)) },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onClick = { viewModel.handleEvent(AppEvent.ToggleEthernetTunnel(tunnelConf)) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-40
@@ -1,40 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.components
|
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.PhoneAndroid
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MobileDataTunnelItem(tunnelConf: TunnelConf, viewModel: AppViewModel): SelectionItem {
|
|
||||||
return SelectionItem(
|
|
||||||
leadingIcon = Icons.Outlined.PhoneAndroid,
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.mobile_tunnel),
|
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
description = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.mobile_data_tunnel),
|
|
||||||
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
trailing = {
|
|
||||||
ScaledSwitch(
|
|
||||||
checked = tunnelConf.isMobileDataTunnel,
|
|
||||||
onClick = { viewModel.handleEvent(AppEvent.ToggleMobileDataTunnel(tunnelConf)) },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onClick = { viewModel.handleEvent(AppEvent.ToggleMobileDataTunnel(tunnelConf)) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-76
@@ -1,76 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.Security
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.TrustedNetworkTextBox
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.WildcardsLabel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun WifiTunnelItem(
|
|
||||||
tunnelConf: TunnelConf,
|
|
||||||
appSettings: AppSettings,
|
|
||||||
viewModel: AppViewModel,
|
|
||||||
currentText: String,
|
|
||||||
onTextChange: (String) -> Unit,
|
|
||||||
): SelectionItem {
|
|
||||||
return SelectionItem(
|
|
||||||
title = {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.Start,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 8.dp),
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Security,
|
|
||||||
contentDescription = stringResource(R.string.use_tunnel_on_wifi_name),
|
|
||||||
modifier = Modifier.size(iconSize),
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.use_tunnel_on_wifi_name),
|
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(start = 16.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
description = {
|
|
||||||
TrustedNetworkTextBox(
|
|
||||||
trustedNetworks = tunnelConf.tunnelNetworks,
|
|
||||||
onDelete = { viewModel.handleEvent(AppEvent.DeleteTunnelRunSSID(it, tunnelConf)) },
|
|
||||||
currentText = currentText,
|
|
||||||
onSave = {
|
|
||||||
viewModel.handleEvent(AppEvent.AddTunnelRunSSID(it, tunnelConf))
|
|
||||||
onTextChange("") // Reset the text field after saving
|
|
||||||
},
|
|
||||||
onValueChange = onTextChange,
|
|
||||||
supporting = {
|
|
||||||
if (appSettings.isWildcardsEnabled) {
|
|
||||||
WildcardsLabel()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.Bolt
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AutoTunnelRowItem(appUiState: AppUiState, onToggle: () -> Unit) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
ExpandingRowListItem(
|
||||||
|
leading = {
|
||||||
|
val icon = Icons.Rounded.Bolt
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
icon.name,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.size(16.dp).scale(1.5f),
|
||||||
|
tint =
|
||||||
|
if (!appUiState.autoTunnelActive) {
|
||||||
|
Color.Gray
|
||||||
|
} else {
|
||||||
|
SilverTree
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = stringResource(R.string.auto_tunneling),
|
||||||
|
trailing = {
|
||||||
|
ScaledSwitch(
|
||||||
|
appUiState.appSettings.isAutoTunnelEnabled,
|
||||||
|
onClick = {
|
||||||
|
onToggle()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
if (context.isRunningOnTv()) {
|
||||||
|
onToggle()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isExpanded = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
-25
@@ -8,12 +8,10 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.ContentPasteGo
|
import androidx.compose.material.icons.filled.ContentPasteGo
|
||||||
import androidx.compose.material.icons.filled.Create
|
import androidx.compose.material.icons.filled.Create
|
||||||
import androidx.compose.material.icons.filled.FileOpen
|
import androidx.compose.material.icons.filled.FileOpen
|
||||||
import androidx.compose.material.icons.filled.Link
|
|
||||||
import androidx.compose.material.icons.filled.QrCode
|
import androidx.compose.material.icons.filled.QrCode
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
@@ -35,14 +33,12 @@ fun TunnelImportSheet(
|
|||||||
onQrClick: () -> Unit,
|
onQrClick: () -> Unit,
|
||||||
onManualImportClick: () -> Unit,
|
onManualImportClick: () -> Unit,
|
||||||
onClipboardClick: () -> Unit,
|
onClipboardClick: () -> Unit,
|
||||||
onUrlClick: () -> Unit,
|
|
||||||
) {
|
) {
|
||||||
val sheetState = rememberModalBottomSheetState()
|
val sheetState = rememberModalBottomSheetState()
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
if (show) {
|
if (show) {
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
},
|
||||||
@@ -114,27 +110,6 @@ fun TunnelImportSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
Row(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
onDismiss()
|
|
||||||
onUrlClick()
|
|
||||||
}
|
|
||||||
.padding(10.dp),
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Filled.Link,
|
|
||||||
contentDescription = stringResource(id = R.string.add_from_url),
|
|
||||||
modifier = Modifier.padding(10.dp),
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
stringResource(id = R.string.add_from_url),
|
|
||||||
modifier = Modifier.padding(10.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
HorizontalDivider()
|
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
|
|||||||
-80
@@ -1,80 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.compose.foundation.gestures.ScrollableDefaults
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.overscroll
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.getValueById
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
|
||||||
import java.text.Collator
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
@Composable
|
|
||||||
fun TunnelList(
|
|
||||||
appUiState: AppUiState,
|
|
||||||
activeTunnels: Map<TunnelConf, TunnelState>,
|
|
||||||
selectedTunnel: TunnelConf?,
|
|
||||||
onSetSelectedTunnel: (TunnelConf?) -> Unit,
|
|
||||||
onDeleteTunnel: (TunnelConf) -> Unit,
|
|
||||||
onToggleTunnel: (TunnelConf, Boolean) -> Unit,
|
|
||||||
onExpandStats: () -> Unit,
|
|
||||||
onCopyTunnel: (TunnelConf) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
viewModel: AppViewModel,
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val collator = Collator.getInstance(Locale.getDefault())
|
|
||||||
val sortedTunnels = remember(appUiState.tunnels) {
|
|
||||||
appUiState.tunnels.sortedWith(compareBy(collator) { it.tunName })
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyColumn(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement = Arrangement.spacedBy(5.dp, Alignment.Top),
|
|
||||||
modifier = modifier
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
if (appUiState.tunnels.isEmpty()) return@pointerInput
|
|
||||||
}.overscroll(ScrollableDefaults.overscrollEffect()),
|
|
||||||
state = rememberLazyListState(0, appUiState.tunnels.count()),
|
|
||||||
userScrollEnabled = true,
|
|
||||||
reverseLayout = false,
|
|
||||||
flingBehavior = ScrollableDefaults.flingBehavior(),
|
|
||||||
) {
|
|
||||||
if (appUiState.tunnels.isEmpty()) {
|
|
||||||
item {
|
|
||||||
GettingStartedLabel(onClick = { context.openWebUrl(it) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items(sortedTunnels, key = { it.id }) { tunnel ->
|
|
||||||
val tunnelState = activeTunnels.getValueById(tunnel.id) ?: TunnelState()
|
|
||||||
TunnelRowItem(
|
|
||||||
isActive = tunnelState.status.isUpOrStarting(),
|
|
||||||
expanded = appUiState.generalState.isTunnelStatsExpanded,
|
|
||||||
isSelected = selectedTunnel?.id == tunnel.id,
|
|
||||||
tunnel = tunnel,
|
|
||||||
tunnelState = tunnelState,
|
|
||||||
onSetSelectedTunnel = { onSetSelectedTunnel(it) },
|
|
||||||
onClick = onExpandStats,
|
|
||||||
onCopy = { onCopyTunnel(tunnel) },
|
|
||||||
onDelete = { onDeleteTunnel(tunnel) },
|
|
||||||
onSwitchClick = { checked -> onToggleTunnel(tunnel, checked) },
|
|
||||||
viewModel = viewModel,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+63
-88
@@ -1,8 +1,7 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.focusable
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
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
|
||||||
@@ -24,7 +23,6 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
@@ -33,11 +31,9 @@ import com.zaneschepke.wireguardautotunnel.ui.Route
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem
|
import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asColor
|
import com.zaneschepke.wireguardautotunnel.util.extensions.asColor
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TunnelRowItem(
|
fun TunnelRowItem(
|
||||||
@@ -46,164 +42,142 @@ fun TunnelRowItem(
|
|||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
tunnel: TunnelConf,
|
tunnel: TunnelConf,
|
||||||
tunnelState: TunnelState,
|
tunnelState: TunnelState,
|
||||||
onSetSelectedTunnel: (TunnelConf?) -> Unit,
|
onHold: () -> Unit,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onCopy: () -> Unit,
|
onCopy: () -> Unit,
|
||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
onSwitchClick: (Boolean) -> Unit,
|
onSwitchClick: (checked: Boolean) -> Unit,
|
||||||
viewModel: AppViewModel,
|
|
||||||
) {
|
) {
|
||||||
|
val leadingIconColor = if (!isActive) Color.Gray else tunnelState.statistics.asColor()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val snackbar = SnackbarController.current
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
val itemFocusRequester = remember { FocusRequester() }
|
val itemFocusRequester = remember { FocusRequester() }
|
||||||
val isTv = context.isRunningOnTv()
|
|
||||||
|
|
||||||
val leadingIconColor = if (!isActive) Color.Gray else tunnelState.statistics.asColor()
|
|
||||||
|
|
||||||
val (leadingIcon, size) = when {
|
|
||||||
tunnel.isPrimaryTunnel -> Pair(Icons.Rounded.Star, 16.dp)
|
|
||||||
tunnel.isMobileDataTunnel -> Pair(Icons.Rounded.Smartphone, 16.dp)
|
|
||||||
tunnel.isEthernetTunnel -> Pair(Icons.Rounded.SettingsEthernet, 16.dp)
|
|
||||||
else -> Pair(Icons.Rounded.Circle, 14.dp)
|
|
||||||
}
|
|
||||||
|
|
||||||
ExpandingRowListItem(
|
ExpandingRowListItem(
|
||||||
leading = {
|
leading = {
|
||||||
|
val icon = when {
|
||||||
|
tunnel.isPrimaryTunnel -> Icons.Rounded.Star
|
||||||
|
tunnel.isMobileDataTunnel -> Icons.Rounded.Smartphone
|
||||||
|
tunnel.isEthernetTunnel -> Icons.Rounded.SettingsEthernet
|
||||||
|
else -> Icons.Rounded.Circle
|
||||||
|
}
|
||||||
Icon(
|
Icon(
|
||||||
leadingIcon,
|
icon,
|
||||||
stringResource(R.string.status),
|
icon.name,
|
||||||
tint = leadingIconColor,
|
tint = leadingIconColor,
|
||||||
modifier = Modifier.size(size),
|
modifier = Modifier.size(16.dp),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
text = tunnel.tunName,
|
text = tunnel.tunName,
|
||||||
onHold = {
|
onHold = {
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
onSetSelectedTunnel(tunnel)
|
onHold()
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
if (!isTv) {
|
if (!context.isRunningOnTv()) {
|
||||||
if (isActive) onClick()
|
if (isActive) {
|
||||||
|
onClick()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
onSetSelectedTunnel(tunnel)
|
onHold()
|
||||||
itemFocusRequester.requestFocus()
|
itemFocusRequester.requestFocus()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isExpanded = expanded && isActive,
|
isExpanded = expanded && isActive,
|
||||||
expanded = { if (isActive && expanded) TunnelStatisticsRow(tunnelState.statistics, tunnel) },
|
expanded = { if (isActive && expanded) TunnelStatisticsRow(tunnelState.statistics, tunnel) },
|
||||||
trailing = {
|
trailing = {
|
||||||
if (isSelected && !isTv) {
|
if (
|
||||||
Row(
|
isSelected &&
|
||||||
modifier = Modifier.fillMaxWidth(),
|
!context.isRunningOnTv()
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
|
||||||
) {
|
) {
|
||||||
|
Row {
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
onClick = {
|
onClick = {
|
||||||
onSetSelectedTunnel(null)
|
navController.navigate(
|
||||||
navController.navigate(Route.TunnelOptions(tunnel.id))
|
Route.TunnelOptions(tunnel.id),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
val icon = Icons.Rounded.Settings
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Settings,
|
icon,
|
||||||
stringResource(id = R.string.settings),
|
icon.name,
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.focusable(),
|
||||||
onClick = {
|
onClick = { onCopy() },
|
||||||
onCopy()
|
|
||||||
onSetSelectedTunnel(null)
|
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
val icon = Icons.Rounded.CopyAll
|
||||||
Icons.Rounded.CopyAll,
|
Icon(icon, icon.name)
|
||||||
stringResource(R.string.copy),
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
enabled = !isActive,
|
enabled = !isActive,
|
||||||
|
modifier = Modifier.focusable(),
|
||||||
onClick = { onDelete() },
|
onClick = { onDelete() },
|
||||||
) {
|
) {
|
||||||
Icon(
|
val icon = Icons.Rounded.Delete
|
||||||
Icons.Rounded.Delete,
|
Icon(icon, icon.name)
|
||||||
stringResource(R.string.delete_tunnel),
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (isTv) {
|
} else {
|
||||||
Row(
|
if (context.isRunningOnTv()) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
Row {
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
|
||||||
) {
|
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
onClick = {
|
onClick = {
|
||||||
navController.navigate(Route.TunnelOptions(tunnel.id))
|
onHold()
|
||||||
onSetSelectedTunnel(null)
|
navController.navigate(
|
||||||
|
Route.TunnelOptions(tunnel.id),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
val icon = Icons.Rounded.Settings
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Settings,
|
icon,
|
||||||
stringResource(id = R.string.settings),
|
icon.name,
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
onClick = {
|
onClick = {
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
onClick()
|
onClick()
|
||||||
} else {
|
} else {
|
||||||
viewModel.handleEvent(
|
snackbar.showMessage(
|
||||||
AppEvent.ShowMessage(StringValue.StringResource(R.string.turn_on_tunnel)),
|
context.getString(R.string.turn_on_tunnel),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Icon(
|
val icon = Icons.Rounded.Info
|
||||||
Icons.Rounded.Info,
|
Icon(icon, icon.name)
|
||||||
stringResource(R.string.info),
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.weight(1f),
|
onClick = { onCopy() },
|
||||||
onClick = onCopy,
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
val icon = Icons.Rounded.CopyAll
|
||||||
Icons.Rounded.CopyAll,
|
Icon(icon, icon.name)
|
||||||
stringResource(R.string.copy),
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
onClick = {
|
onClick = {
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
viewModel.handleEvent(
|
snackbar.showMessage(
|
||||||
AppEvent.ShowMessage(StringValue.StringResource(R.string.turn_off_tunnel)),
|
context.getString(R.string.turn_off_tunnel),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
onHold()
|
||||||
onDelete()
|
onDelete()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
val icon = Icons.Rounded.Delete
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Delete,
|
icon,
|
||||||
stringResource(R.string.delete_tunnel),
|
icon.name,
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ScaledSwitch(
|
ScaledSwitch(
|
||||||
modifier = Modifier
|
modifier = Modifier.focusRequester(itemFocusRequester),
|
||||||
.focusRequester(itemFocusRequester)
|
|
||||||
.weight(1f),
|
|
||||||
checked = isActive,
|
checked = isActive,
|
||||||
onClick = onSwitchClick,
|
onClick = onSwitchClick,
|
||||||
)
|
)
|
||||||
@@ -215,6 +189,7 @@ fun TunnelRowItem(
|
|||||||
onClick = onSwitchClick,
|
onClick = onSwitchClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
-55
@@ -1,55 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun UrlImportDialog(onDismiss: () -> Unit, onConfirm: (String) -> Unit) {
|
|
||||||
var url by remember { mutableStateOf("") }
|
|
||||||
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
title = { Text(stringResource(R.string.add_from_url)) },
|
|
||||||
text = {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 8.dp),
|
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = url,
|
|
||||||
onValueChange = { url = it },
|
|
||||||
label = { Text(stringResource(R.string.enter_config_url)) },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = { onConfirm(url) },
|
|
||||||
enabled = url.isNotBlank(),
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.okay))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = onDismiss) {
|
|
||||||
Text(stringResource(R.string.cancel))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-108
@@ -1,108 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.config
|
|
||||||
|
|
||||||
import android.view.WindowManager
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components.AddPeerButton
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components.InterfaceSection
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components.PeersSection
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ConfigScreen(tunnelConf: TunnelConf?, appViewModel: AppViewModel, viewModel: ConfigViewModel = hiltViewModel()) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
|
||||||
|
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
val activity = context as? MainActivity
|
|
||||||
|
|
||||||
// Secure screen due to sensitive information
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
activity?.window?.setFlags(
|
|
||||||
WindowManager.LayoutParams.FLAG_SECURE,
|
|
||||||
WindowManager.LayoutParams.FLAG_SECURE,
|
|
||||||
)
|
|
||||||
onDispose {
|
|
||||||
activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
// set callback for navbar to invoke save
|
|
||||||
appViewModel.handleEvent(
|
|
||||||
AppEvent.SetScreenAction {
|
|
||||||
keyboardController?.hide()
|
|
||||||
viewModel.save(tunnelConf)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(tunnelConf) {
|
|
||||||
viewModel.initFromTunnel(tunnelConf)
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(uiState.success) {
|
|
||||||
if (uiState.success == true) {
|
|
||||||
appViewModel.handleEvent(AppEvent.ShowMessage(StringValue.StringResource(R.string.config_changes_saved)))
|
|
||||||
appViewModel.handleEvent(AppEvent.PopBackStack(true))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(uiState.message) {
|
|
||||||
uiState.message?.let { message ->
|
|
||||||
appViewModel.handleEvent(AppEvent.ShowMessage(message))
|
|
||||||
viewModel.setMessage(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uiState.showAuthPrompt) {
|
|
||||||
AuthorizationPrompt(
|
|
||||||
onSuccess = {
|
|
||||||
viewModel.toggleShowAuthPrompt()
|
|
||||||
viewModel.onAuthenticated()
|
|
||||||
},
|
|
||||||
onError = {
|
|
||||||
viewModel.toggleShowAuthPrompt()
|
|
||||||
appViewModel.handleEvent(AppEvent.ShowMessage(StringValue.StringResource(R.string.error_authentication_failed)))
|
|
||||||
},
|
|
||||||
onFailure = {
|
|
||||||
viewModel.toggleShowAuthPrompt()
|
|
||||||
appViewModel.handleEvent(AppEvent.ShowMessage(StringValue.StringResource(R.string.error_authorization_failed)))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.padding(horizontal = 24.dp),
|
|
||||||
) {
|
|
||||||
InterfaceSection(uiState, viewModel)
|
|
||||||
PeersSection(uiState, viewModel)
|
|
||||||
AddPeerButton(viewModel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-173
@@ -1,173 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.config
|
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.state.ConfigUiState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.PeerProxy
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class ConfigViewModel @Inject constructor(
|
|
||||||
private val tunnelRepository: TunnelRepository,
|
|
||||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(ConfigUiState())
|
|
||||||
val uiState: StateFlow<ConfigUiState> = _uiState.asStateFlow()
|
|
||||||
|
|
||||||
fun initFromTunnel(tunnelConf: TunnelConf?) {
|
|
||||||
if (tunnelConf == null) return
|
|
||||||
_uiState.update {
|
|
||||||
val proxy = ConfigProxy.from(tunnelConf.toAmConfig())
|
|
||||||
it.copy(
|
|
||||||
tunnelName = tunnelConf.name,
|
|
||||||
configProxy = proxy,
|
|
||||||
showScripts = proxy.hasScripts(),
|
|
||||||
showAmneziaValues = proxy.`interface`.junkPacketCount.isNotBlank(),
|
|
||||||
isAuthenticated = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateTunnelName(name: String) {
|
|
||||||
_uiState.update {
|
|
||||||
it.copy(tunnelName = name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateInterface(newInterface: InterfaceProxy) {
|
|
||||||
_uiState.update {
|
|
||||||
it.copy(
|
|
||||||
configProxy = it.configProxy.copy(
|
|
||||||
`interface` = newInterface,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toggleAmneziaValues() {
|
|
||||||
_uiState.update {
|
|
||||||
it.copy(showAmneziaValues = !it.showAmneziaValues)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toggleScripts() {
|
|
||||||
_uiState.update {
|
|
||||||
it.copy(showScripts = !it.showScripts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toggleAmneziaCompatibility() {
|
|
||||||
val (show, `interface`) = with(_uiState.value.configProxy) {
|
|
||||||
if (`interface`.isAmneziaCompatibilityModeSet()) {
|
|
||||||
Pair(false, `interface`.resetAmneziaProperties())
|
|
||||||
} else {
|
|
||||||
Pair(true, `interface`.toAmneziaCompatibilityConfig())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_uiState.update {
|
|
||||||
it.copy(
|
|
||||||
showAmneziaValues = show,
|
|
||||||
configProxy = it.configProxy.copy(
|
|
||||||
`interface` = `interface`,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addPeer() {
|
|
||||||
_uiState.update { currentState ->
|
|
||||||
currentState.copy(
|
|
||||||
configProxy = currentState.configProxy.copy(
|
|
||||||
peers = currentState.configProxy.peers + PeerProxy(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removePeer(index: Int) {
|
|
||||||
_uiState.update { currentState ->
|
|
||||||
currentState.copy(
|
|
||||||
configProxy = currentState.configProxy.copy(
|
|
||||||
peers = currentState.configProxy.peers.toMutableList().apply { removeAt(index) },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updatePeer(index: Int, peer: PeerProxy) {
|
|
||||||
_uiState.update { currentState ->
|
|
||||||
currentState.copy(
|
|
||||||
configProxy = currentState.configProxy.copy(
|
|
||||||
peers = currentState.configProxy.peers.toMutableList().apply { set(index, peer) },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toggleLanExclusion(index: Int) {
|
|
||||||
val peer = _uiState.value.configProxy.peers[index]
|
|
||||||
val updated = if (peer.isLanExcluded()) peer.includeLan() else peer.excludeLan()
|
|
||||||
updatePeer(index, updated)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setMessage(message: StringValue?) {
|
|
||||||
_uiState.update {
|
|
||||||
it.copy(message = message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO improve error messaging
|
|
||||||
fun save(tunnelConf: TunnelConf?) = viewModelScope.launch(ioDispatcher) {
|
|
||||||
val message = try {
|
|
||||||
val saveConfig = buildTunnelConfFromState(tunnelConf)
|
|
||||||
tunnelRepository.save(saveConfig)
|
|
||||||
_uiState.update { it.copy(success = true) }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
setMessage(
|
|
||||||
e.message?.let { message ->
|
|
||||||
(StringValue.DynamicString(message))
|
|
||||||
} ?: StringValue.StringResource(R.string.unknown_error),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildTunnelConfFromState(tunnelConf: TunnelConf?): TunnelConf {
|
|
||||||
val (wg, am) = _uiState.value.configProxy.buildConfigs()
|
|
||||||
val name = _uiState.value.tunnelName
|
|
||||||
return tunnelConf?.copyWithCallback(
|
|
||||||
tunName = name,
|
|
||||||
amQuick = am.toAwgQuickString(true),
|
|
||||||
wgQuick = wg.toWgQuickString(true),
|
|
||||||
) ?: TunnelConf(
|
|
||||||
tunName = name, amQuick = am.toAwgQuickString(true),
|
|
||||||
wgQuick = wg.toWgQuickString(true),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onAuthenticated() {
|
|
||||||
_uiState.update {
|
|
||||||
it.copy(isAuthenticated = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toggleShowAuthPrompt() {
|
|
||||||
_uiState.update {
|
|
||||||
it.copy(showAuthPrompt = !it.showAuthPrompt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-28
@@ -1,28 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.ConfigViewModel
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun AddPeerButton(viewModel: ConfigViewModel) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxWidth().padding(bottom = 24.dp),
|
|
||||||
) {
|
|
||||||
TextButton(onClick = { viewModel.addPeer() }) {
|
|
||||||
Text(stringResource(R.string.add_peer))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-72
@@ -1,72 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.rounded.MoreVert
|
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.shadow
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun InterfaceDropdown(
|
|
||||||
expanded: Boolean,
|
|
||||||
onExpandedChange: (Boolean) -> Unit,
|
|
||||||
showScripts: Boolean,
|
|
||||||
showAmneziaValues: Boolean,
|
|
||||||
isAmneziaCompatibilitySet: Boolean,
|
|
||||||
onToggleScripts: () -> Unit,
|
|
||||||
onToggleAmneziaValues: () -> Unit,
|
|
||||||
onToggleAmneziaCompatibility: () -> Unit,
|
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
IconButton(
|
|
||||||
modifier = Modifier.size(iconSize),
|
|
||||||
onClick = { onExpandedChange(true) },
|
|
||||||
) {
|
|
||||||
Icon(Icons.Rounded.MoreVert, contentDescription = "More")
|
|
||||||
}
|
|
||||||
DropdownMenu(
|
|
||||||
expanded = expanded,
|
|
||||||
onDismissRequest = { onExpandedChange(false) },
|
|
||||||
modifier = Modifier.shadow(12.dp).background(MaterialTheme.colorScheme.surface),
|
|
||||||
) {
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(if (showScripts) stringResource(R.string.hide_scripts) else stringResource(R.string.show_scripts)) },
|
|
||||||
onClick = {
|
|
||||||
onToggleScripts()
|
|
||||||
onExpandedChange(false)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(if (showAmneziaValues) stringResource(R.string.hide_amnezia_properties) else stringResource(R.string.show_amnezia_properties)) },
|
|
||||||
onClick = {
|
|
||||||
onToggleAmneziaValues()
|
|
||||||
onExpandedChange(false)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = {
|
|
||||||
Text(
|
|
||||||
if (isAmneziaCompatibilitySet) stringResource(R.string.remove_amnezia_compatibility) else stringResource(R.string.enable_amnezia_compatibility),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
onToggleAmneziaCompatibility()
|
|
||||||
onExpandedChange(false)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user