mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c8adb380b | |||
| 614f97fd14 | |||
| fbd470f5d2 | |||
| 5f89b2ed31 | |||
| 9503a3284b | |||
| 68c1a19bd3 | |||
| f3bb6667c3 | |||
| 244a990c37 | |||
| cbf07600b4 | |||
| ec8d90d13d | |||
| 85acca8604 | |||
| 0a9773d202 | |||
| 3cb4480a65 | |||
| a7f3255a76 | |||
| 7d7b99f448 | |||
| 74e9e462bb | |||
| 619e3c1cde | |||
| 77f8a8215b | |||
| 8772036dd7 | |||
| 63625ccbd7 | |||
| 9ac7ae77b3 | |||
| e062fbb34d | |||
| 16d5586433 | |||
| 48a3ad64f4 | |||
| e5796d641d | |||
| daf5eebdd2 | |||
| 4c725491f4 | |||
| 7529c11172 | |||
| 83f530df42 | |||
| 8083ab9526 | |||
| 7d1312da0f | |||
| d4dbc43c70 | |||
| 294f2624c7 | |||
| 0603cb2fdd | |||
| 48ddbcbb0e | |||
| e6c3e3f5b3 | |||
| 0d75699b40 | |||
| 5c98aab9e0 | |||
| a1e3489ba2 | |||
| bcd19b5494 | |||
| 160a6ca84d | |||
| aaf7ebd326 | |||
| b8c75a45e4 | |||
| ac17a09e19 | |||
| c51a7ee393 | |||
| c534516e33 | |||
| 9c999cc62c | |||
| cc3c865211 | |||
| 8648a67fdc | |||
| 9ee1fa69ed | |||
| 379ffdcbbf | |||
| 6e3c1324b2 | |||
| 660bea0104 | |||
| 2b8610fa8a | |||
| 944034ac74 | |||
| 9f394aeffb |
@@ -70,16 +70,16 @@ jobs:
|
||||
outputs:
|
||||
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up JDK 17
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
java-version: '21'
|
||||
cache: gradle
|
||||
|
||||
- name: Grant execute permission for gradlew
|
||||
|
||||
@@ -72,15 +72,15 @@ jobs:
|
||||
outputs:
|
||||
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
- name: Set up JDK 17
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
java-version: '21'
|
||||
cache: gradle
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
@@ -112,6 +112,9 @@ jobs:
|
||||
./gradlew :app:assemble${flavor^}Debug --stacktrace
|
||||
;;
|
||||
esac
|
||||
env:
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
GITHUB_RUN_NUMBER: ${{ github.run_number }}
|
||||
- name: Get release apk path
|
||||
id: apk-path
|
||||
run: echo "path=$(find . -regex '^.*/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
has_new_commits: ${{ steps.check.outputs.new_commits }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
- name: Check for new commits
|
||||
id: check
|
||||
env:
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
name: on-pr
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
format_check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up JDK 17
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Verify Gradle Wrapper
|
||||
uses: gradle/actions/wrapper-validation@v6
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
java-version: '21'
|
||||
cache: gradle
|
||||
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Run ktfmt
|
||||
run: ./gradlew ktfmtCheck
|
||||
run: ./gradlew ktfmtCheck
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
name: publish-github
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event_name == 'push' && github.ref || 'master' }}
|
||||
- name: Install system dependencies
|
||||
@@ -205,7 +205,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-google-aab
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event_name == 'push' && github.ref || 'master' }}
|
||||
|
||||
|
||||
+38
-32
@@ -1,6 +1,5 @@
|
||||
import com.android.build.api.dsl.ApplicationExtension
|
||||
import com.android.build.api.variant.FilterConfiguration
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
@@ -23,13 +22,6 @@ licensee {
|
||||
ignoreDependencies("com.github.topjohnwu.libsu")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_17
|
||||
freeCompilerArgs = listOf("-XXLanguage:+PropertyParamAnnotationDefaultTargetMode")
|
||||
}
|
||||
}
|
||||
|
||||
configure<ApplicationExtension> {
|
||||
namespace = Constants.APP_ID
|
||||
compileSdk = Constants.TARGET_SDK
|
||||
@@ -46,10 +38,11 @@ configure<ApplicationExtension> {
|
||||
|
||||
splits {
|
||||
abi {
|
||||
isEnable = !project.hasProperty("noSplits")
|
||||
val noSplits = providers.gradleProperty("noSplits").isPresent
|
||||
isEnable = !noSplits
|
||||
reset()
|
||||
include("armeabi-v7a", "arm64-v8a")
|
||||
isUniversalApk = !project.hasProperty("noSplits")
|
||||
isUniversalApk = !noSplits
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,14 +50,17 @@ configure<ApplicationExtension> {
|
||||
applicationId = Constants.APP_ID
|
||||
minSdk = Constants.MIN_SDK
|
||||
targetSdk = Constants.TARGET_SDK
|
||||
versionCode = computeVersionCode()
|
||||
versionName = computeVersionName()
|
||||
versionCode = Constants.VERSION_CODE
|
||||
versionName = Constants.VERSION_NAME
|
||||
|
||||
experimentalProperties["android.experimental.disableGitVersion"] = true
|
||||
|
||||
sourceSets {
|
||||
getByName("debug").assets.directories += "$projectDir/schemas"
|
||||
}
|
||||
|
||||
val languagesArray = buildLanguagesArray(languageList())
|
||||
val languagesProvider = project.languageListProvider()
|
||||
val languagesArray = buildLanguagesArray(languagesProvider.get())
|
||||
buildConfigField("String[]", "LANGUAGES", "new String[]{ $languagesArray }")
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
@@ -134,8 +130,6 @@ configure<ApplicationExtension> {
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
@@ -148,31 +142,42 @@ configure<ApplicationExtension> {
|
||||
|
||||
androidComponents {
|
||||
onVariants { variant ->
|
||||
val isNightly = project.isNightlyBuild()
|
||||
|
||||
val abiNameMap =
|
||||
mapOf(
|
||||
"armeabi-v7a" to "armv7",
|
||||
"arm64-v8a" to "arm64",
|
||||
"x86" to "x86",
|
||||
"x86_64" to "x64",
|
||||
)
|
||||
if (isNightly) {
|
||||
variant.outputs.forEach { output ->
|
||||
|
||||
output.versionCode.set(
|
||||
output.versionCode.get() + project.getVersionCodeIncrement()
|
||||
)
|
||||
|
||||
val currentVersion = output.versionName.get()
|
||||
val nextVersion = bumpToNextPatchVersion(currentVersion)
|
||||
val gitHash = project.getGitCommitHash()
|
||||
|
||||
output.versionName.set("$nextVersion-nightly+git.$gitHash")
|
||||
}
|
||||
}
|
||||
|
||||
val abiNameMap = mapOf(
|
||||
"armeabi-v7a" to "armv7",
|
||||
"arm64-v8a" to "arm64",
|
||||
"x86" to "x86",
|
||||
"x86_64" to "x64",
|
||||
)
|
||||
|
||||
variant.outputs.forEach { output ->
|
||||
val abi = output.filters.find { it.filterType == FilterConfiguration.FilterType.ABI }?.identifier
|
||||
|
||||
val flavorName = variant.productFlavors.joinToString("-") { it.second }
|
||||
|
||||
val versionName = output.versionName.get()
|
||||
|
||||
val baseFileName = "${Constants.APP_NAME}-${flavorName}-v${versionName}"
|
||||
|
||||
val outputFileName =
|
||||
if (!abi.isNullOrEmpty()) {
|
||||
val shortAbiName = abiNameMap.getOrDefault(abi, abi)
|
||||
"${baseFileName}-${shortAbiName}.apk"
|
||||
} else {
|
||||
"${baseFileName}.apk"
|
||||
}
|
||||
val outputFileName = if (!abi.isNullOrEmpty()) {
|
||||
val shortAbiName = abiNameMap.getOrDefault(abi, abi)
|
||||
"${baseFileName}-${shortAbiName}.apk"
|
||||
} else {
|
||||
"${baseFileName}.apk"
|
||||
}
|
||||
|
||||
output.outputFileName.set(outputFileName)
|
||||
}
|
||||
@@ -225,6 +230,7 @@ dependencies {
|
||||
// UI utilities
|
||||
implementation(libs.bundles.ui.utilities)
|
||||
implementation(libs.lottie.compose)
|
||||
implementation(libs.sonner)
|
||||
|
||||
// Misc utilities
|
||||
implementation(libs.bundles.misc.utilities)
|
||||
|
||||
@@ -0,0 +1,513 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 31,
|
||||
"identityHash": "1dee3799f1c6526c48723fd2fee58d11",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "tunnel_config",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `quick_config` TEXT NOT NULL DEFAULT '', `dynamic_dns` INTEGER NOT NULL DEFAULT false, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `prefer_ipv6` INTEGER NOT NULL DEFAULT false, `position` INTEGER NOT NULL DEFAULT 0, `auto_tunnel_apps` TEXT NOT NULL DEFAULT '[]', `is_metered` INTEGER NOT NULL DEFAULT false, `ipv4_fallback` INTEGER NOT NULL DEFAULT false, `ipv6_restore` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelNetworks",
|
||||
"columnName": "tunnel_networks",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMobileDataTunnel",
|
||||
"columnName": "is_mobile_data_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPrimaryTunnel",
|
||||
"columnName": "is_primary_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "quickConfig",
|
||||
"columnName": "quick_config",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "dynamicDnsEnabled",
|
||||
"columnName": "dynamic_dns",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isEthernetTunnel",
|
||||
"columnName": "is_ethernet_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isIpv6Preferred",
|
||||
"columnName": "prefer_ipv6",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "position",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "autoTunnelApps",
|
||||
"columnName": "auto_tunnel_apps",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "'[]'"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMetered",
|
||||
"columnName": "is_metered",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "ipv4FallbackEnabled",
|
||||
"columnName": "ipv4_fallback",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "ipv6RestoreEnabled",
|
||||
"columnName": "ipv6_restore",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_tunnel_config_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tunnel_config_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "proxy_settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `socks5_proxy_enabled` INTEGER NOT NULL DEFAULT 0, `socks5_proxy_bind_address` TEXT, `http_proxy_enable` INTEGER NOT NULL DEFAULT 0, `http_proxy_bind_address` TEXT, `proxy_username` TEXT, `proxy_password` TEXT)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "socks5ProxyEnabled",
|
||||
"columnName": "socks5_proxy_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "socks5ProxyBindAddress",
|
||||
"columnName": "socks5_proxy_bind_address",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "httpProxyEnabled",
|
||||
"columnName": "http_proxy_enable",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "httpProxyBindAddress",
|
||||
"columnName": "http_proxy_bind_address",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "proxyUsername",
|
||||
"columnName": "proxy_username",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "proxyPassword",
|
||||
"columnName": "proxy_password",
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "general_settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT 0, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT 0, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `global_split_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `app_mode` INTEGER NOT NULL DEFAULT 0, `theme` TEXT NOT NULL DEFAULT 'AUTOMATIC', `locale` TEXT, `remote_key` TEXT, `is_remote_control_enabled` INTEGER NOT NULL DEFAULT 0, `is_pin_lock_enabled` INTEGER NOT NULL DEFAULT 0, `is_always_on_vpn_enabled` INTEGER NOT NULL DEFAULT 0, `already_donated` INTEGER NOT NULL DEFAULT 0, `screen_recording_security` INTEGER NOT NULL DEFAULT 1, `global_amnezia_enabled` INTEGER NOT NULL DEFAULT 0, `tunnel_scripting_enabled` INTEGER NOT NULL DEFAULT 0)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isShortcutsEnabled",
|
||||
"columnName": "is_shortcuts_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isRestoreOnBootEnabled",
|
||||
"columnName": "is_restore_on_boot_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMultiTunnelEnabled",
|
||||
"columnName": "is_multi_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isGlobalSplitTunnelEnabled",
|
||||
"columnName": "global_split_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelMode",
|
||||
"columnName": "app_mode",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "theme",
|
||||
"columnName": "theme",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "'AUTOMATIC'"
|
||||
},
|
||||
{
|
||||
"fieldPath": "locale",
|
||||
"columnName": "locale",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "remoteKey",
|
||||
"columnName": "remote_key",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isRemoteControlEnabled",
|
||||
"columnName": "is_remote_control_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPinLockEnabled",
|
||||
"columnName": "is_pin_lock_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||
"columnName": "is_always_on_vpn_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "alreadyDonated",
|
||||
"columnName": "already_donated",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "screenRecordingSecurityEnabled",
|
||||
"columnName": "screen_recording_security",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "1"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isGlobalAmneziaEnabled",
|
||||
"columnName": "global_amnezia_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelScriptingEnabled",
|
||||
"columnName": "tunnel_scripting_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "auto_tunnel_settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL DEFAULT 0, `trusted_network_ssids` TEXT NOT NULL DEFAULT '', `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT 0, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT 0, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT 0, `wifi_detection_method` INTEGER NOT NULL DEFAULT 0, `start_on_boot` INTEGER NOT NULL DEFAULT 0, `disable_on_captive_portal` INTEGER NOT NULL DEFAULT 1)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelEnabled",
|
||||
"columnName": "is_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "trustedNetworkSSIDs",
|
||||
"columnName": "trusted_network_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnWifiEnabled",
|
||||
"columnName": "is_tunnel_on_wifi_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isWildcardsEnabled",
|
||||
"columnName": "is_wildcards_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isStopOnNoInternetEnabled",
|
||||
"columnName": "is_stop_on_no_internet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnUnsecureEnabled",
|
||||
"columnName": "is_tunnel_on_unsecure_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "wifiDetectionMethod",
|
||||
"columnName": "wifi_detection_method",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "startOnBoot",
|
||||
"columnName": "start_on_boot",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "disableTunnelOnCaptivePortal",
|
||||
"columnName": "disable_on_captive_portal",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "1"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "monitoring_settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_local_logs_enabled` INTEGER NOT NULL DEFAULT 0, `tunnel_statistics_enabled` INTEGER NOT NULL DEFAULT 1, `tunnel_statistics_poll_interval` INTEGER NOT NULL DEFAULT 3)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isLocalLogsEnabled",
|
||||
"columnName": "is_local_logs_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelStatisticsEnabled",
|
||||
"columnName": "tunnel_statistics_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "1"
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelStatisticsPollInterval",
|
||||
"columnName": "tunnel_statistics_poll_interval",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "3"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "dns_settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dns_protocol` INTEGER NOT NULL DEFAULT 0, `dns_endpoint` TEXT, `global_tunnel_dns_enabled` INTEGER NOT NULL DEFAULT 0)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dnsProtocol",
|
||||
"columnName": "dns_protocol",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "dnsEndpoint",
|
||||
"columnName": "dns_endpoint",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isGlobalTunnelDnsEnabled",
|
||||
"columnName": "global_tunnel_dns_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "lockdown_settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bypass_lan` INTEGER NOT NULL DEFAULT 0, `metered` INTEGER NOT NULL DEFAULT 0, `dual_stack` INTEGER NOT NULL DEFAULT 0)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "bypassLan",
|
||||
"columnName": "bypass_lan",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "metered",
|
||||
"columnName": "metered",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "dualStack",
|
||||
"columnName": "dual_stack",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1dee3799f1c6526c48723fd2fee58d11')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,6 @@
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.App.Start"
|
||||
tools:targetApi="tiramisu">
|
||||
@@ -74,6 +73,13 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="wg" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<action android:name="android.intent.action.SHOW_APP_INFO" />
|
||||
@@ -169,9 +175,9 @@
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
<service
|
||||
android:name=".core.service.tile.TunnelControlTile"
|
||||
android:name=".service.tile.TunnelControlTile"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_notification"
|
||||
android:icon="@drawable/ic_qs_logo"
|
||||
android:label="@string/tunnel_control"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<meta-data
|
||||
@@ -186,9 +192,9 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".core.service.tile.AutoTunnelControlTile"
|
||||
android:name=".service.tile.AutoTunnelControlTile"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_notification"
|
||||
android:icon="@drawable/ic_qs_logo"
|
||||
android:label="@string/auto_tunnel"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<meta-data
|
||||
@@ -203,7 +209,7 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".core.service.autotunnel.AutoTunnelService"
|
||||
android:name=".service.autotunnel.AutoTunnelService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse"
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
import ProxySettingsScreen
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
@@ -19,17 +21,29 @@ import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.CheckCircleOutline
|
||||
import androidx.compose.material.icons.outlined.ErrorOutline
|
||||
import androidx.compose.material.icons.outlined.FavoriteBorder
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material.icons.outlined.WarningAmber
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -38,20 +52,19 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||
@@ -59,6 +72,10 @@ import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import com.dokar.sonner.TextToastAction
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.dokar.sonner.Toaster
|
||||
import com.dokar.sonner.rememberToasterState
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
@@ -69,11 +86,9 @@ import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.banner.AppAlertBanner
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.LocalNetworkPermissionDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarInfo
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarType
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.rememberCustomSnackbarState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.SecureRoute
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Tab
|
||||
@@ -110,19 +125,29 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.ipv6.IPv6
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.sort.SortScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.SplitTunnelScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.AlertRed
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Heart
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.OffWhite
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Straw
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.installApk
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.restartApp
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
||||
import com.zaneschepke.wireguardautotunnel.util.permission.LocalNetworkPermissionHelper
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigEditViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
|
||||
import de.raphaelebner.roomdatabasebackup.core.OnCompleteListener.Companion.EXIT_CODE_ERROR
|
||||
import de.raphaelebner.roomdatabasebackup.core.OnCompleteListener.Companion.EXIT_CODE_ERROR_DECRYPTION_ERROR
|
||||
import de.raphaelebner.roomdatabasebackup.core.OnCompleteListener.Companion.EXIT_CODE_ERROR_RESTORE_BACKUP_IS_ENCRYPTED
|
||||
import de.raphaelebner.roomdatabasebackup.core.OnCompleteListener.Companion.EXIT_CODE_ERROR_WRONG_DECRYPTION_PASSWORD
|
||||
import de.raphaelebner.roomdatabasebackup.core.RoomBackup
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
@@ -132,6 +157,7 @@ import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.orbitmvi.orbit.compose.collectAsState
|
||||
import timber.log.Timber
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
@@ -155,9 +181,10 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
handleIncomingIntent(intent)
|
||||
roomBackup = RoomBackup(this).database(appDatabase).enableLogDebug(true).maxFileCount(5)
|
||||
|
||||
roomBackup = RoomBackup(this)
|
||||
handleConfigFileIntent(intent)
|
||||
handleWgDeepLinkIntent(intent)
|
||||
|
||||
installSplashScreen().apply {
|
||||
setKeepOnScreenCondition { !viewModel.container.stateFlow.value.isAppLoaded }
|
||||
@@ -175,12 +202,54 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
val snackbarState = rememberCustomSnackbarState()
|
||||
val toaster = rememberToasterState()
|
||||
var showVpnPermissionDialog by remember { mutableStateOf(false) }
|
||||
var vpnPermissionDenied by remember { mutableStateOf(false) }
|
||||
var requestingTunnelMode by remember {
|
||||
mutableStateOf<Pair<TunnelMode?, TunnelConfig?>>(Pair(null, null))
|
||||
}
|
||||
var showLocalNetworkRationale by remember { mutableStateOf(false) }
|
||||
var hasPromptedLocalNetwork by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val localNetworkPermissionLauncher =
|
||||
rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.RequestPermission()
|
||||
) { isGranted ->
|
||||
if (!isGranted) {
|
||||
val canAskAgain =
|
||||
ActivityCompat.shouldShowRequestPermissionRationale(
|
||||
this,
|
||||
Manifest.permission.ACCESS_LOCAL_NETWORK,
|
||||
)
|
||||
|
||||
if (!canAskAgain) {
|
||||
val intent =
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = Uri.fromParts("package", packageName, null)
|
||||
}
|
||||
startActivity(intent)
|
||||
} else {
|
||||
toaster.show(
|
||||
message =
|
||||
context.getString(R.string.local_network_permission_denied),
|
||||
type = ToastType.Warning,
|
||||
duration = 6000.milliseconds,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(uiState.isAppLoaded) {
|
||||
if (
|
||||
uiState.isAppLoaded &&
|
||||
!hasPromptedLocalNetwork &&
|
||||
LocalNetworkPermissionHelper.shouldRequestPermission() &&
|
||||
!LocalNetworkPermissionHelper.isPermissionGranted(context)
|
||||
) {
|
||||
hasPromptedLocalNetwork = true
|
||||
showLocalNetworkRationale = true
|
||||
}
|
||||
}
|
||||
|
||||
val startingStack = buildList {
|
||||
add(Route.Tunnels)
|
||||
@@ -232,22 +301,18 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
is GlobalSideEffect.Snackbar -> {
|
||||
scope.launch {
|
||||
snackbarState.showSnackbar(
|
||||
SnackbarInfo(
|
||||
message =
|
||||
buildAnnotatedString {
|
||||
append(sideEffect.message.asString(context))
|
||||
},
|
||||
type = sideEffect.type ?: SnackbarType.INFO,
|
||||
durationMs = sideEffect.durationMs ?: 4000L,
|
||||
)
|
||||
)
|
||||
when (sideEffect.type) {
|
||||
ToastType.Warning,
|
||||
ToastType.Error -> toaster.dismissAll()
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
is GlobalSideEffect.Toast ->
|
||||
scope.launch { context.showToast(sideEffect.message.asString(context)) }
|
||||
toaster.show(
|
||||
message = sideEffect.message.asString(context),
|
||||
type = sideEffect.type,
|
||||
duration = (sideEffect.durationMs ?: 4000L).milliseconds,
|
||||
)
|
||||
}
|
||||
|
||||
is GlobalSideEffect.LaunchUrl -> context.openWebUrl(sideEffect.url)
|
||||
is GlobalSideEffect.InstallApk -> context.installApk(sideEffect.apk)
|
||||
@@ -275,49 +340,58 @@ class MainActivity : AppCompatActivity() {
|
||||
},
|
||||
)
|
||||
|
||||
val annotatedMessage = buildAnnotatedString {
|
||||
append(context.getString(R.string.donation_prompt_prefix))
|
||||
append(" ")
|
||||
withLink(
|
||||
LinkAnnotation.Clickable(
|
||||
tag = context.getString(R.string.support),
|
||||
styles =
|
||||
TextLinkStyles(
|
||||
style =
|
||||
SpanStyle(
|
||||
textDecoration = TextDecoration.Underline,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
focusedStyle =
|
||||
SpanStyle(
|
||||
textDecoration = TextDecoration.Underline,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
background =
|
||||
MaterialTheme.colorScheme.primary.copy(
|
||||
alpha = 0.2f
|
||||
),
|
||||
),
|
||||
),
|
||||
) {
|
||||
snackbarState.dismissCurrent()
|
||||
navController.push(Route.Donate)
|
||||
}
|
||||
) {
|
||||
append(context.getString(R.string.donation_prompt_link))
|
||||
}
|
||||
append(" ")
|
||||
append(context.getString(R.string.donation_prompt_suffix))
|
||||
if (showLocalNetworkRationale) {
|
||||
LocalNetworkPermissionDialog(
|
||||
onDismiss = {
|
||||
showLocalNetworkRationale = false
|
||||
toaster.show(
|
||||
message =
|
||||
context.getString(R.string.local_network_permission_denied),
|
||||
type = ToastType.Warning,
|
||||
duration = 6000.milliseconds,
|
||||
)
|
||||
},
|
||||
onAttest = {
|
||||
showLocalNetworkRationale = false
|
||||
|
||||
localNetworkPermissionLauncher.launch(
|
||||
Manifest.permission.ACCESS_LOCAL_NETWORK
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
uiState.pendingWgImportUrl?.let { url ->
|
||||
val host = Uri.parse(url).host ?: url
|
||||
InfoDialog(
|
||||
onDismiss = { viewModel.dismissWgImport() },
|
||||
onAttest = { viewModel.importFromUrl(url) },
|
||||
title = stringResource(R.string.add_from_url),
|
||||
body = { Text(stringResource(R.string.wg_url_confirm_message, host)) },
|
||||
confirmText = stringResource(R.string.okay),
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (uiState.shouldShowDonationSnackbar && !uiState.alreadyDonated) {
|
||||
viewModel.setShouldShowDonationSnackbar(false)
|
||||
snackbarState.showSnackbar(
|
||||
SnackbarInfo(
|
||||
message = annotatedMessage,
|
||||
type = SnackbarType.THANK_YOU,
|
||||
durationMs = 30_000L,
|
||||
)
|
||||
toaster.show(
|
||||
message =
|
||||
context.getString(R.string.donation_prompt_prefix) +
|
||||
" " +
|
||||
context.getString(R.string.donation_prompt_link) +
|
||||
" " +
|
||||
context.getString(R.string.donation_prompt_suffix),
|
||||
type = ToastType.Normal,
|
||||
duration = 30_000L.milliseconds,
|
||||
action =
|
||||
TextToastAction(
|
||||
text = context.getString(R.string.donate_title),
|
||||
onClick = { toastId ->
|
||||
toaster.dismiss(toastId)
|
||||
navController.push(Route.Donate)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -378,25 +452,6 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
Scaffold(
|
||||
snackbarHost = {
|
||||
snackbarState.SnackbarHost(
|
||||
modifier =
|
||||
Modifier.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 80.dp)
|
||||
) { info ->
|
||||
CustomSnackBar(
|
||||
message = info.message,
|
||||
type = info.type,
|
||||
onDismiss = { snackbarState.dismissCurrent() },
|
||||
containerColor =
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||
2.dp
|
||||
),
|
||||
modifier =
|
||||
Modifier.wrapContentHeight(align = Alignment.Top),
|
||||
)
|
||||
}
|
||||
},
|
||||
topBar = { DynamicTopAppBar(navState) },
|
||||
bottomBar = {
|
||||
if (navState.showBottomItems) {
|
||||
@@ -548,6 +603,70 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
}
|
||||
Toaster(
|
||||
state = toaster,
|
||||
alignment = Alignment.BottomCenter,
|
||||
offset = IntOffset(0, -220),
|
||||
richColors = true,
|
||||
background = {
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp),
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp),
|
||||
)
|
||||
)
|
||||
},
|
||||
elevation = 1.dp,
|
||||
shadowAmbientColor =
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f),
|
||||
shadowSpotColor =
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||
border = {
|
||||
BorderStroke(
|
||||
0.dp,
|
||||
androidx.compose.ui.graphics.Color.Transparent,
|
||||
)
|
||||
},
|
||||
actionSlot = { toast ->
|
||||
(toast.action as? TextToastAction)?.let { action ->
|
||||
TextButton(
|
||||
onClick = { action.onClick(toast) },
|
||||
colors =
|
||||
ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.primary
|
||||
),
|
||||
contentPadding = PaddingValues(horizontal = 12.dp),
|
||||
) {
|
||||
Text(text = action.text, fontWeight = FontWeight.Medium)
|
||||
}
|
||||
}
|
||||
},
|
||||
iconSlot = { toast ->
|
||||
val (icon, color) =
|
||||
when (toast.type) {
|
||||
ToastType.Success ->
|
||||
Icons.Outlined.CheckCircleOutline to SilverTree
|
||||
ToastType.Error ->
|
||||
Icons.Outlined.ErrorOutline to AlertRed
|
||||
ToastType.Warning ->
|
||||
Icons.Outlined.WarningAmber to Straw
|
||||
ToastType.Info ->
|
||||
Icons.Outlined.Info to
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
ToastType.Normal ->
|
||||
Icons.Outlined.FavoriteBorder to Heart
|
||||
}
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = color,
|
||||
modifier = Modifier.padding(end = 12.dp),
|
||||
)
|
||||
},
|
||||
contentColor = { MaterialTheme.colorScheme.onSurface },
|
||||
shape = { RoundedCornerShape(16.dp) },
|
||||
showCloseButton = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -555,55 +674,14 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
fun performBackup() = lifecycleScope.launch {
|
||||
roomBackup
|
||||
.database(appDatabase)
|
||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||
.enableLogDebug(true)
|
||||
.maxFileCount(5)
|
||||
.apply {
|
||||
onCompleteListener { success, _, _ ->
|
||||
lifecycleScope.launch {
|
||||
if (success) {
|
||||
showToast(
|
||||
getString(
|
||||
R.string.backup_success,
|
||||
getString(R.string.restarting_app),
|
||||
)
|
||||
)
|
||||
restartApp()
|
||||
} else {
|
||||
showToast(R.string.backup_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun handleWgDeepLinkIntent(intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_VIEW) {
|
||||
val uri = intent.data ?: return
|
||||
if (uri.scheme == "wg") {
|
||||
val httpsUrl = uri.toString().replaceFirst("wg://", "https://")
|
||||
viewModel.promptWgImport(httpsUrl)
|
||||
}
|
||||
.backup()
|
||||
}
|
||||
|
||||
fun performRestore() = lifecycleScope.launch {
|
||||
roomBackup
|
||||
.database(appDatabase)
|
||||
.enableLogDebug(true)
|
||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||
.apply {
|
||||
onCompleteListener { success, _, _ ->
|
||||
lifecycleScope.launch {
|
||||
if (success) {
|
||||
showToast(
|
||||
getString(
|
||||
R.string.restore_success,
|
||||
getString(R.string.restarting_app),
|
||||
)
|
||||
)
|
||||
restartApp()
|
||||
} else {
|
||||
showToast(R.string.restore_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.restore()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -611,21 +689,105 @@ class MainActivity : AppCompatActivity() {
|
||||
networkMonitor.checkPermissionsAndUpdateState()
|
||||
}
|
||||
|
||||
fun performBackup(encrypt: Boolean = false, password: String? = null) {
|
||||
roomBackup
|
||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||
.apply {
|
||||
if (encrypt && !password.isNullOrBlank()) {
|
||||
backupIsEncrypted(true)
|
||||
customEncryptPassword(password)
|
||||
}
|
||||
}
|
||||
.onCompleteListener { success, _, _ ->
|
||||
lifecycleScope.launch {
|
||||
val sideEffect =
|
||||
if (success) {
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.backup_success),
|
||||
ToastType.Success,
|
||||
)
|
||||
} else {
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.backup_failed),
|
||||
ToastType.Error,
|
||||
)
|
||||
}
|
||||
viewModel.postSideEffect(sideEffect)
|
||||
}
|
||||
}
|
||||
.backup()
|
||||
}
|
||||
|
||||
fun performRestore(encrypt: Boolean = false, password: String? = null) {
|
||||
roomBackup
|
||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||
.apply {
|
||||
if (encrypt && !password.isNullOrBlank()) {
|
||||
backupIsEncrypted(true)
|
||||
customEncryptPassword(password)
|
||||
}
|
||||
}
|
||||
.onCompleteListener { success, message, exitCode ->
|
||||
lifecycleScope.launch {
|
||||
if (success) {
|
||||
viewModel.postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.restore_success),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
roomBackup.restartApp(Intent(this@MainActivity, MainActivity::class.java))
|
||||
} else {
|
||||
Timber.w("Restore failed, exitCode=$exitCode, message=$message")
|
||||
|
||||
val errorMessage =
|
||||
when (exitCode) {
|
||||
EXIT_CODE_ERROR_WRONG_DECRYPTION_PASSWORD ->
|
||||
getString(R.string.restore_failed_wrong_password)
|
||||
|
||||
EXIT_CODE_ERROR,
|
||||
EXIT_CODE_ERROR_DECRYPTION_ERROR,
|
||||
EXIT_CODE_ERROR_RESTORE_BACKUP_IS_ENCRYPTED ->
|
||||
getString(R.string.restore_failed_invalid_file)
|
||||
|
||||
else -> getString(R.string.restore_failed)
|
||||
}
|
||||
|
||||
viewModel.postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.DynamicString(errorMessage),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.restore()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
handleIncomingIntent(intent)
|
||||
handleConfigFileIntent(intent)
|
||||
handleWgDeepLinkIntent(intent)
|
||||
}
|
||||
|
||||
private fun handleIncomingIntent(intent: Intent?) {
|
||||
private fun handleConfigFileIntent(intent: Intent?) {
|
||||
intent ?: return
|
||||
|
||||
when (intent.action) {
|
||||
Intent.ACTION_VIEW,
|
||||
Intent.ACTION_EDIT,
|
||||
Intent.ACTION_SEND -> {
|
||||
val uri: Uri? = intent.data
|
||||
uri?.let { viewModel.importFromUri(it) }
|
||||
val uri: Uri? = intent.data ?: return
|
||||
val name = uri?.lastPathSegment?.lowercase() ?: return
|
||||
if (
|
||||
!name.endsWith(FileUtils.CONF_FILE_EXTENSION) &&
|
||||
!name.endsWith(FileUtils.ZIP_FILE_EXTENSION)
|
||||
) {
|
||||
Timber.d("Ignoring non-config URI in handleIncomingIntent: $uri")
|
||||
return
|
||||
}
|
||||
viewModel.importFromUri(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,8 @@ import com.zaneschepke.tunnel.backend.Backend
|
||||
import com.zaneschepke.tunnel.di.tunnelModule
|
||||
import com.zaneschepke.tunnel.service.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.core.event.TunnelEventDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AppBoostrapCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||
import com.zaneschepke.wireguardautotunnel.di.Dispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.di.Scope
|
||||
@@ -21,6 +18,9 @@ import com.zaneschepke.wireguardautotunnel.di.dispatchersModule
|
||||
import com.zaneschepke.wireguardautotunnel.di.networkModule
|
||||
import com.zaneschepke.wireguardautotunnel.di.tunnelBackendProviderModule
|
||||
import com.zaneschepke.wireguardautotunnel.di.workerModule
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -51,6 +51,13 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
||||
|
||||
private val backend: Backend by inject()
|
||||
|
||||
private val alwaysOnCallback =
|
||||
object : VpnService.AlwaysOnCallback {
|
||||
override fun alwaysOnTriggered() {
|
||||
applicationScope.launch { tunnelCoordinator.startDefault() }
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(KoinViewModelScopeApi::class)
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
@@ -71,11 +78,10 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
||||
lazyModules(networkModule)
|
||||
}
|
||||
instance = this
|
||||
|
||||
notificationService.createAllChannels()
|
||||
|
||||
// Sync tiles
|
||||
AutoTunnelTileRefresher.refresh(this)
|
||||
TunnelTileRefresher.refresh(this)
|
||||
syncTiles()
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
@@ -87,13 +93,7 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
||||
Timber.plant(ReleaseTree())
|
||||
}
|
||||
|
||||
backend.setAlwaysOnCallback(
|
||||
object : VpnService.AlwaysOnCallback {
|
||||
override fun alwaysOnTriggered() {
|
||||
applicationScope.launch { tunnelCoordinator.startDefault() }
|
||||
}
|
||||
}
|
||||
)
|
||||
backend.setAlwaysOnCallback(alwaysOnCallback)
|
||||
|
||||
val dispatcher = get<TunnelEventDispatcher>()
|
||||
val coordinator = get<TunnelCoordinator>()
|
||||
@@ -111,6 +111,11 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
||||
applicationScope.launch(ioDispatcher) { boostrapCoordinator.bootstrap() }
|
||||
}
|
||||
|
||||
private fun syncTiles() {
|
||||
AutoTunnelTileRefresher.refresh(this)
|
||||
TunnelTileRefresher.refresh(this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var instance: WireGuardAutoTunnel
|
||||
private set
|
||||
|
||||
+1
-1
@@ -3,11 +3,11 @@ package com.zaneschepke.wireguardautotunnel.core.broadcast
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.di.Scope
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
|
||||
+156
-18
@@ -1,14 +1,19 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.event
|
||||
|
||||
import android.content.Context
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.tunnel.event.TunnelEvent
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.tunnel.state.BackendStatus
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.TunnelNotificationLine
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.TunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.lifecyle.AppVisibilityObserver
|
||||
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationLine
|
||||
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
@@ -20,11 +25,14 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class TunnelEventDispatcher(
|
||||
private val notificationManager: TunnelNotificationService,
|
||||
private val tunnelRepository: TunnelRepository,
|
||||
private val context: Context,
|
||||
private val appVisibilityObserver: AppVisibilityObserver,
|
||||
private val globalEffectRepository: GlobalEffectRepository,
|
||||
) {
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
@@ -36,54 +44,174 @@ class TunnelEventDispatcher(
|
||||
tunnelDisplayStates: StateFlow<Map<Int, DisplayTunnelState>>,
|
||||
) {
|
||||
|
||||
// informational events
|
||||
// Informational events from tunnel backend
|
||||
providerEvents
|
||||
.distinctUntilChanged()
|
||||
.onEach { event ->
|
||||
when (event) {
|
||||
is TunnelEvent.FallbackToIpv4 -> {
|
||||
val name = getTunnelName(event.tunnelId)
|
||||
notificationManager.showIpv4Fallback(name)
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message =
|
||||
StringValue.DynamicString(
|
||||
context.getString(
|
||||
R.string.notification_ipv4_fallback_message,
|
||||
name,
|
||||
)
|
||||
),
|
||||
type = ToastType.Info,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = { notificationManager.showIpv4Fallback(name) },
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelEvent.RecoveredToIpv6 -> {
|
||||
val name = getTunnelName(event.tunnelId)
|
||||
notificationManager.showIpv6Recovery(name)
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message =
|
||||
StringValue.DynamicString(
|
||||
context.getString(
|
||||
R.string.notification_ipv6_recovery_message,
|
||||
name,
|
||||
)
|
||||
),
|
||||
type = ToastType.Success,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = { notificationManager.showIpv6Recovery(name) },
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelEvent.DynamicDnsUpdate -> {
|
||||
val name = getTunnelName(event.tunnelId)
|
||||
notificationManager.showDynamicDnsUpdate(name)
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message =
|
||||
StringValue.DynamicString(
|
||||
context.getString(
|
||||
R.string.notification_dynamic_dns_message,
|
||||
name,
|
||||
)
|
||||
),
|
||||
type = ToastType.Info,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = { notificationManager.showDynamicDnsUpdate(name) },
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelEvent.NoRootShellAccess -> {
|
||||
notificationManager.showRootShellAccess()
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message =
|
||||
StringValue.DynamicString(
|
||||
context.getString(R.string.error_root_denied)
|
||||
),
|
||||
type = ToastType.Error,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = { notificationManager.showRootShellAccess() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(scope)
|
||||
|
||||
// errors from the coordinator
|
||||
// Errors from our tunnel coordinator
|
||||
coordinatorErrors
|
||||
.distinctUntilChanged()
|
||||
.onEach { error ->
|
||||
when (error) {
|
||||
is TunnelErrorEvent.VpnPermissionDenied -> {
|
||||
notificationManager.showVpnRequired()
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message =
|
||||
StringValue.DynamicString(
|
||||
context.getString(R.string.vpn_permission_required)
|
||||
),
|
||||
type = ToastType.Error,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = { notificationManager.showVpnRequired() },
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelErrorEvent.InternalFailure -> {
|
||||
notificationManager.showError(error.message)
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message = StringValue.DynamicString(error.message),
|
||||
type = ToastType.Error,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = { notificationManager.showError(error.message) },
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelErrorEvent.Socks5PortUnavailable -> {
|
||||
val name = getTunnelName(error.tunnelId)
|
||||
notificationManager.showSocks5PortUnavailable(error.port, name)
|
||||
val message =
|
||||
context.getString(R.string.error_socks5_port_unavailable, error.port)
|
||||
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message = StringValue.DynamicString(message),
|
||||
type = ToastType.Error,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = {
|
||||
notificationManager.showSocks5PortUnavailable(error.port, name)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelErrorEvent.HttpPortUnavailable -> {
|
||||
val name = getTunnelName(error.tunnelId)
|
||||
notificationManager.showHttpPortUnavailable(error.port, name)
|
||||
val message =
|
||||
context.getString(R.string.error_http_port_unavailable, error.port)
|
||||
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message = StringValue.DynamicString(message),
|
||||
type = ToastType.Error,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = {
|
||||
notificationManager.showHttpPortUnavailable(error.port, name)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,8 +234,7 @@ class TunnelEventDispatcher(
|
||||
val tunnel = allTunnels.find { it.id == id } ?: return@mapNotNull null
|
||||
|
||||
val displayState =
|
||||
displayStates[id]
|
||||
?: DisplayTunnelState.from(activeTunnel, System.currentTimeMillis())
|
||||
displayStates[id] ?: DisplayTunnelState.from(activeTunnel)
|
||||
|
||||
TunnelNotificationLine(
|
||||
id = id,
|
||||
@@ -135,8 +262,7 @@ class TunnelEventDispatcher(
|
||||
|
||||
val tunnel = allTunnels.find { it.id == id } ?: return@mapNotNull null
|
||||
val displayState =
|
||||
displayStates[id]
|
||||
?: DisplayTunnelState.from(activeTunnel, System.currentTimeMillis())
|
||||
displayStates[id] ?: DisplayTunnelState.from(activeTunnel)
|
||||
|
||||
TunnelNotificationLine(
|
||||
id = id,
|
||||
@@ -154,6 +280,18 @@ class TunnelEventDispatcher(
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
private fun showOrNotify(
|
||||
scope: CoroutineScope,
|
||||
foregroundAction: suspend () -> Unit,
|
||||
backgroundAction: () -> Unit,
|
||||
) {
|
||||
if (appVisibilityObserver.isForeground.value) {
|
||||
scope.launch { foregroundAction() }
|
||||
} else {
|
||||
backgroundAction()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getTunnelName(tunnelId: Int): String {
|
||||
return tunnelRepository.getById(tunnelId)?.name ?: context.getString(R.string.unknown)
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.orchestration
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||
|
||||
class AutoTunnelCoordinator(
|
||||
private val repository: AutoTunnelSettingsRepository,
|
||||
|
||||
+36
-9
@@ -2,7 +2,6 @@ package com.zaneschepke.wireguardautotunnel.core.orchestration
|
||||
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.wireguardautotunnel.core.event.TunnelErrorEvent
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.RoomDnsSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelActionSource
|
||||
@@ -10,13 +9,16 @@ import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.TunnelActionEvent
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.MonitoringSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -44,6 +46,7 @@ class TunnelCoordinator(
|
||||
dnsSettingsRepository: RoomDnsSettingsRepository,
|
||||
monitoringSettingsRepository: MonitoringSettingsRepository,
|
||||
proxyRepository: ProxySettingsRepository,
|
||||
lockdownModeRepository: LockdownSettingsRepository,
|
||||
scope: CoroutineScope,
|
||||
) {
|
||||
|
||||
@@ -55,7 +58,7 @@ class TunnelCoordinator(
|
||||
tunnelProvider.backendStatus
|
||||
.map { status ->
|
||||
status.activeTunnels.mapValues { (_, activeTunnel) ->
|
||||
DisplayTunnelState.from(activeTunnel, System.currentTimeMillis())
|
||||
DisplayTunnelState.from(activeTunnel)
|
||||
}
|
||||
}
|
||||
.debounce(400L.milliseconds)
|
||||
@@ -66,6 +69,7 @@ class TunnelCoordinator(
|
||||
val dns: DnsSettings,
|
||||
val monitoring: MonitoringSettings,
|
||||
val proxy: ProxySettings,
|
||||
val lockdown: LockdownSettings,
|
||||
)
|
||||
|
||||
private val runtimeSettingsSnapshot =
|
||||
@@ -74,12 +78,14 @@ class TunnelCoordinator(
|
||||
dnsSettingsRepository.flow,
|
||||
monitoringSettingsRepository.flow,
|
||||
proxyRepository.flow,
|
||||
) { general, dns, monitoring, proxy ->
|
||||
lockdownModeRepository.flow,
|
||||
) { general, dns, monitoring, proxy, lockdown ->
|
||||
RuntimeSettingsSnapshot(
|
||||
general = general,
|
||||
dns = dns,
|
||||
monitoring = monitoring,
|
||||
proxy = proxy,
|
||||
lockdown = lockdown,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -117,7 +123,7 @@ class TunnelCoordinator(
|
||||
|
||||
// enforce single tunnel, for now
|
||||
if (backendStatus.value.activeTunnels.isNotEmpty()) {
|
||||
stopActiveTunnelsInternal()
|
||||
stopActiveTunnelsInternal(source)
|
||||
}
|
||||
|
||||
startTunnelInternal(config, source)
|
||||
@@ -131,7 +137,13 @@ class TunnelCoordinator(
|
||||
stopTunnelInternal(id, source)
|
||||
}
|
||||
|
||||
suspend fun stopActiveTunnels() = tunnelMutex.withLock { stopActiveTunnelsInternal() }
|
||||
suspend fun stopActiveTunnels(source: TunnelActionSource = TunnelActionSource.USER) =
|
||||
tunnelMutex.withLock {
|
||||
if (source == TunnelActionSource.USER) {
|
||||
_userOverrideFlow.tryEmit(Unit)
|
||||
}
|
||||
stopActiveTunnelsInternal(source)
|
||||
}
|
||||
|
||||
private suspend fun startTunnelInternal(
|
||||
tunnelConfig: TunnelConfig,
|
||||
@@ -143,6 +155,7 @@ class TunnelCoordinator(
|
||||
val dnsSettings = snapshot.dns
|
||||
val proxySettings = snapshot.proxy
|
||||
val monitoringSettings = snapshot.monitoring
|
||||
val lockdownSettings = snapshot.lockdown
|
||||
|
||||
val config = tunnelConfig.getConfig()
|
||||
val policy =
|
||||
@@ -178,8 +191,10 @@ class TunnelCoordinator(
|
||||
}
|
||||
|
||||
TunnelMode.LOCK_DOWN -> {
|
||||
|
||||
BackendMode.Proxy.KillSwitchPrimary(runConfig)
|
||||
BackendMode.Proxy.KillSwitchPrimary(
|
||||
runConfig,
|
||||
lockdownSettings.toKillSwitchConfig(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,6 +221,10 @@ class TunnelCoordinator(
|
||||
|
||||
suspend fun toggleTunnels(source: TunnelActionSource = TunnelActionSource.USER) =
|
||||
tunnelMutex.withLock {
|
||||
if (source == TunnelActionSource.USER) {
|
||||
_userOverrideFlow.tryEmit(Unit)
|
||||
}
|
||||
|
||||
val active = tunnelProvider.backendStatus.value.activeTunnels
|
||||
if (active.isNotEmpty()) {
|
||||
lastActiveTunnels = active.keys.toList()
|
||||
@@ -214,7 +233,7 @@ class TunnelCoordinator(
|
||||
_actions.emit(TunnelActionEvent.Stopped(tunnelId = id, source = source))
|
||||
}
|
||||
|
||||
stopActiveTunnelsInternal()
|
||||
stopActiveTunnelsInternal(source)
|
||||
return@withLock
|
||||
}
|
||||
|
||||
@@ -239,7 +258,15 @@ class TunnelCoordinator(
|
||||
.onFailure { _errors.emit(TunnelErrorEvent.from(it, id)) }
|
||||
}
|
||||
|
||||
private suspend fun stopActiveTunnelsInternal() {
|
||||
private suspend fun stopActiveTunnelsInternal(
|
||||
source: TunnelActionSource = TunnelActionSource.USER
|
||||
) {
|
||||
val active = tunnelProvider.backendStatus.value.activeTunnels
|
||||
|
||||
active.keys.forEach { id ->
|
||||
_actions.emit(TunnelActionEvent.Stopped(tunnelId = id, source = source))
|
||||
}
|
||||
|
||||
tunnelProvider.stopActiveTunnels()
|
||||
}
|
||||
}
|
||||
|
||||
+3
-4
@@ -19,9 +19,8 @@ class ShortcutsActivity : ComponentActivity() {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
applicationScope.launch {
|
||||
shortcutCoordinator.handle(intent)
|
||||
finish()
|
||||
}
|
||||
finish()
|
||||
|
||||
applicationScope.launch { shortcutCoordinator.handle(intent) }
|
||||
}
|
||||
}
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.tunnel.ApplicationProvider
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.tunnel.state.BackendStatus
|
||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.PROXY_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.VPN_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationLine
|
||||
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
class AndroidApplicationProvider(
|
||||
private val notificationService: NotificationService,
|
||||
private val tunnelNotificationService: TunnelNotificationService,
|
||||
private val tunnelRepository: TunnelRepository,
|
||||
) : ApplicationProvider {
|
||||
|
||||
private val context: Context = notificationService.context
|
||||
|
||||
override fun refreshTile(context: Context) {
|
||||
TunnelTileRefresher.refresh(context)
|
||||
}
|
||||
|
||||
override fun createVpnConfigurePendingIntent(context: Context): PendingIntent {
|
||||
return PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
Intent(context, MainActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
},
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
||||
)
|
||||
}
|
||||
|
||||
override val vpnInitNotification: Notification
|
||||
get() =
|
||||
notificationService.createNotification(
|
||||
channel = AndroidNotificationService.NotificationChannels.Tunnel.VPN,
|
||||
title = context.getString(R.string.initializing),
|
||||
onGoing = true,
|
||||
groupKey = VPN_GROUP_KEY,
|
||||
)
|
||||
|
||||
override val proxyInitNotification: Notification
|
||||
get() =
|
||||
notificationService.createNotification(
|
||||
channel = AndroidNotificationService.NotificationChannels.Tunnel.Proxy,
|
||||
title = context.getString(R.string.initializing),
|
||||
onGoing = true,
|
||||
groupKey = PROXY_GROUP_KEY,
|
||||
)
|
||||
|
||||
override val vpnNotificationId: Int
|
||||
get() = NotificationService.VPN_NOTIFICATION_ID
|
||||
|
||||
override val proxyNotificationId: Int
|
||||
get() = NotificationService.PROXY_NOTIFICATION_ID
|
||||
|
||||
override suspend fun buildVpnPersistentNotification(
|
||||
currentStatus: BackendStatus
|
||||
): Notification {
|
||||
val lines = computeVpnNotificationLines(currentStatus)
|
||||
return tunnelNotificationService.buildVpnPersistentNotification(lines)
|
||||
}
|
||||
|
||||
override suspend fun buildProxyPersistentNotification(
|
||||
currentStatus: BackendStatus
|
||||
): Notification {
|
||||
val lines = computeProxyNotificationLines(currentStatus)
|
||||
return tunnelNotificationService.buildProxyPersistentNotification(lines)
|
||||
}
|
||||
|
||||
private suspend fun computeVpnNotificationLines(
|
||||
status: BackendStatus
|
||||
): Map<Int, TunnelNotificationLine> {
|
||||
val activeTunnels = status.activeTunnels
|
||||
val allTunnels = tunnelRepository.userTunnelsFlow.first()
|
||||
return activeTunnels
|
||||
.mapNotNull { (id, activeTunnel) ->
|
||||
val mode = activeTunnel.mode ?: return@mapNotNull null
|
||||
if (mode !is BackendMode.Vpn && mode !is BackendMode.Proxy.KillSwitchPrimary)
|
||||
return@mapNotNull null
|
||||
val tunnel = allTunnels.find { it.id == id } ?: return@mapNotNull null
|
||||
val displayState = DisplayTunnelState.from(activeTunnel)
|
||||
TunnelNotificationLine(id, tunnel.name, displayState)
|
||||
}
|
||||
.associateBy { it.id }
|
||||
}
|
||||
|
||||
private suspend fun computeProxyNotificationLines(
|
||||
status: BackendStatus
|
||||
): Map<Int, TunnelNotificationLine> {
|
||||
val activeTunnels = status.activeTunnels
|
||||
val allTunnels = tunnelRepository.userTunnelsFlow.first()
|
||||
return activeTunnels
|
||||
.mapNotNull { (id, activeTunnel) ->
|
||||
val mode = activeTunnel.mode ?: return@mapNotNull null
|
||||
if (mode !is BackendMode.Proxy.Standard) return@mapNotNull null
|
||||
val tunnel = allTunnels.find { it.id == id } ?: return@mapNotNull null
|
||||
val displayState = DisplayTunnelState.from(activeTunnel)
|
||||
TunnelNotificationLine(id, tunnel.name, displayState)
|
||||
}
|
||||
.associateBy { it.id }
|
||||
}
|
||||
}
|
||||
-9
@@ -4,14 +4,11 @@ import com.zaneschepke.tunnel.Tunnel
|
||||
import com.zaneschepke.tunnel.backend.Backend
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.tunnel.state.BackendStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
@@ -52,10 +49,4 @@ class TunnelBackendProvider(
|
||||
override suspend fun disableLockDown(): Result<Unit> {
|
||||
return backend.disableKillSwitch()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val localErrorEvents = MutableSharedFlow<Pair<String?, BackendCoreException>>()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val localMessageEvents = MutableSharedFlow<Pair<String?, BackendMessage>>()
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||
import java.util.concurrent.TimeUnit
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
|
||||
DnsSettings::class,
|
||||
LockdownSettings::class,
|
||||
],
|
||||
version = 30,
|
||||
version = 31,
|
||||
autoMigrations =
|
||||
[
|
||||
AutoMigration(from = 1, to = 2),
|
||||
@@ -63,6 +63,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
|
||||
AutoMigration(from = 26, to = 27, spec = GlobalsMigration::class),
|
||||
AutoMigration(from = 27, to = 28, spec = DonationMigration::class),
|
||||
AutoMigration(from = 29, to = 30, spec = SingleConfigMigration::class),
|
||||
AutoMigration(from = 30, to = 31),
|
||||
],
|
||||
exportSchema = true,
|
||||
)
|
||||
|
||||
+3
@@ -18,4 +18,7 @@ interface AutoTunnelSettingsDao {
|
||||
|
||||
@Query("UPDATE auto_tunnel_settings SET is_tunnel_enabled = :enabled")
|
||||
suspend fun updateAutoTunnelEnabled(enabled: Boolean)
|
||||
|
||||
@Query("UPDATE auto_tunnel_settings SET disable_on_captive_portal = :enabled")
|
||||
suspend fun updateDisableOnCaptivePortal(enabled: Boolean)
|
||||
}
|
||||
|
||||
+2
@@ -27,4 +27,6 @@ data class AutoTunnelSettings(
|
||||
@ColumnInfo(name = "wifi_detection_method", defaultValue = "0")
|
||||
val wifiDetectionMethod: WifiDetectionMethod = WifiDetectionMethod.fromValue(0),
|
||||
@ColumnInfo(name = "start_on_boot", defaultValue = "0") val startOnBoot: Boolean = false,
|
||||
@ColumnInfo(name = "disable_on_captive_portal", defaultValue = "1")
|
||||
val disableTunnelOnCaptivePortal: Boolean = true,
|
||||
)
|
||||
|
||||
+4
@@ -26,4 +26,8 @@ class RoomAutoTunnelSettingsRepository(private val autoTunnelSettingsDao: AutoTu
|
||||
override suspend fun updateAutoTunnelEnabled(enabled: Boolean) {
|
||||
autoTunnelSettingsDao.updateAutoTunnelEnabled(enabled)
|
||||
}
|
||||
|
||||
override suspend fun updateDisableOnCaptivePortal(enabled: Boolean) {
|
||||
autoTunnelSettingsDao.updateDisableOnCaptivePortal(enabled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,14 @@ import android.os.StrictMode
|
||||
import com.zaneschepke.logcatter.LogReader
|
||||
import com.zaneschepke.logcatter.LogcatReader
|
||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.DynamicShortcutManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.SelectedTunnelsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
|
||||
|
||||
@@ -27,6 +27,7 @@ val coordinatorModule = module {
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(named(Scope.APPLICATION)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
package com.zaneschepke.wireguardautotunnel.di
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Context
|
||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
||||
import com.zaneschepke.tunnel.NotificationProvider
|
||||
import com.zaneschepke.tunnel.backend.RootShell
|
||||
import com.zaneschepke.tunnel.ApplicationProvider
|
||||
import com.zaneschepke.tunnel.util.RootShell
|
||||
import com.zaneschepke.tunnel.util.RootShellException
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.event.TunnelEventDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService.NotificationChannels
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidTunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.PROXY_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.VPN_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.TunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.AndroidApplicationProvider
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelBackendProvider
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.lifecyle.AppVisibilityObserver
|
||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidTunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.to
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -36,40 +30,15 @@ import timber.log.Timber
|
||||
|
||||
val tunnelBackendProviderModule = module {
|
||||
single<TunnelNotificationService> { AndroidTunnelNotificationService(get()) }
|
||||
single { AppVisibilityObserver() }
|
||||
singleOf(::TunnelEventDispatcher)
|
||||
|
||||
single<NotificationProvider> {
|
||||
val notificationService = get<NotificationService>()
|
||||
val context = androidContext()
|
||||
object : NotificationProvider {
|
||||
override val vpnInitNotification: Notification
|
||||
get() =
|
||||
notificationService.createNotification(
|
||||
channel = NotificationChannels.Tunnel.VPN,
|
||||
title = context.getString(R.string.initializing),
|
||||
onGoing = true,
|
||||
groupKey = VPN_GROUP_KEY,
|
||||
)
|
||||
|
||||
override val proxyInitNotification: Notification
|
||||
get() =
|
||||
notificationService.createNotification(
|
||||
channel = NotificationChannels.Tunnel.Proxy,
|
||||
title = context.getString(R.string.initializing),
|
||||
onGoing = true,
|
||||
groupKey = PROXY_GROUP_KEY,
|
||||
)
|
||||
|
||||
override val vpnNotificationId: Int
|
||||
get() = NotificationService.VPN_NOTIFICATION_ID
|
||||
|
||||
override val proxyNotificationId: Int
|
||||
get() = NotificationService.PROXY_NOTIFICATION_ID
|
||||
|
||||
override fun refreshTile(context: Context) {
|
||||
TunnelTileRefresher.refresh(context)
|
||||
}
|
||||
}
|
||||
single<ApplicationProvider> {
|
||||
AndroidApplicationProvider(
|
||||
notificationService = get(),
|
||||
tunnelNotificationService = get(),
|
||||
tunnelRepository = get(),
|
||||
)
|
||||
}
|
||||
|
||||
single {
|
||||
|
||||
+1
@@ -14,4 +14,5 @@ data class AutoTunnelSettings(
|
||||
val isTunnelOnUnsecureEnabled: Boolean = false,
|
||||
val wifiDetectionMethod: WifiDetectionMethod = WifiDetectionMethod.fromValue(0),
|
||||
val startOnBoot: Boolean = false,
|
||||
val disableTunnelOnCaptivePortal: Boolean = true,
|
||||
)
|
||||
|
||||
+2
@@ -11,4 +11,6 @@ interface AutoTunnelSettingsRepository {
|
||||
suspend fun getAutoTunnelSettings(): AutoTunnelSettings
|
||||
|
||||
suspend fun updateAutoTunnelEnabled(enabled: Boolean)
|
||||
|
||||
suspend fun updateDisableOnCaptivePortal(enabled: Boolean)
|
||||
}
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.asSharedFlow
|
||||
class GlobalEffectRepository {
|
||||
|
||||
private val _globalEffectFlow =
|
||||
MutableSharedFlow<GlobalSideEffect>(replay = 0, extraBufferCapacity = 1)
|
||||
MutableSharedFlow<GlobalSideEffect>(replay = 0, extraBufferCapacity = 0)
|
||||
val flow = _globalEffectFlow.asSharedFlow()
|
||||
|
||||
suspend fun post(effect: GlobalSideEffect) {
|
||||
|
||||
+2
-4
@@ -1,8 +1,8 @@
|
||||
package com.zaneschepke.wireguardautotunnel.domain.sideeffect
|
||||
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarType
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import java.io.File
|
||||
|
||||
@@ -10,14 +10,12 @@ sealed class GlobalSideEffect {
|
||||
|
||||
data class Snackbar(
|
||||
val message: StringValue,
|
||||
val type: SnackbarType? = null,
|
||||
val type: ToastType,
|
||||
val actionLabel: String? = null,
|
||||
val onAction: (() -> Unit)? = null,
|
||||
val durationMs: Long? = null,
|
||||
) : GlobalSideEffect()
|
||||
|
||||
data class Toast(val message: StringValue) : GlobalSideEffect()
|
||||
|
||||
data object PopBackStack : GlobalSideEffect()
|
||||
|
||||
data class LaunchUrl(val url: String) : GlobalSideEffect()
|
||||
|
||||
@@ -11,16 +11,20 @@ sealed class ActiveNetwork {
|
||||
|
||||
data object Cellular : ActiveNetwork()
|
||||
|
||||
data class Wifi(val ssid: String, val isSecure: Boolean?) : ActiveNetwork()
|
||||
data class Wifi(
|
||||
val ssid: String,
|
||||
val isSecure: Boolean?,
|
||||
val requiresCaptivePortalLogin: Boolean,
|
||||
) : ActiveNetwork()
|
||||
}
|
||||
|
||||
data class NetworkState(
|
||||
val activeNetwork: ActiveNetwork = ActiveNetwork.Disconnected,
|
||||
val locationServicesEnabled: Boolean = false,
|
||||
val locationPermissionGranted: Boolean = false,
|
||||
) {
|
||||
fun hasInternet(): Boolean = activeNetwork !is ActiveNetwork.Disconnected
|
||||
}
|
||||
// Has a network that can actually transfer data (not suspended)
|
||||
val hasUsableNetwork: Boolean = false,
|
||||
)
|
||||
|
||||
fun ConnectivityState.toDomain(): NetworkState {
|
||||
val domainNetwork: ActiveNetwork =
|
||||
@@ -33,7 +37,11 @@ fun ConnectivityState.toDomain(): NetworkState {
|
||||
null -> null
|
||||
else -> true
|
||||
}
|
||||
ActiveNetwork.Wifi(ssid = network.ssid, isSecure = isSecure)
|
||||
ActiveNetwork.Wifi(
|
||||
ssid = network.ssid,
|
||||
isSecure = isSecure,
|
||||
requiresCaptivePortalLogin(),
|
||||
)
|
||||
}
|
||||
is MonitorActiveNetwork.Cellular -> ActiveNetwork.Cellular
|
||||
is MonitorActiveNetwork.Ethernet -> ActiveNetwork.Ethernet
|
||||
@@ -44,5 +52,6 @@ fun ConnectivityState.toDomain(): NetworkState {
|
||||
activeNetwork = domainNetwork,
|
||||
locationPermissionGranted = this.locationPermissionsGranted,
|
||||
locationServicesEnabled = this.locationServicesEnabled,
|
||||
hasUsableNetwork = hasUsableNetwork(),
|
||||
)
|
||||
}
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.zaneschepke.wireguardautotunnel.lifecyle
|
||||
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
class AppVisibilityObserver : DefaultLifecycleObserver {
|
||||
|
||||
private val _isForeground = MutableStateFlow(false)
|
||||
val isForeground: StateFlow<Boolean> = _isForeground.asStateFlow()
|
||||
|
||||
init {
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||
}
|
||||
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
_isForeground.value = true
|
||||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
_isForeground.value = false
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
package com.zaneschepke.wireguardautotunnel.notification
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Notification
|
||||
@@ -17,8 +17,8 @@ import androidx.core.app.NotificationManagerCompat.IMPORTANCE_HIGH
|
||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.broadcast.NotificationActionReceiver
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.EXTRA_ID
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.EXTRA_ID
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
|
||||
class AndroidNotificationService(override val context: Context) : NotificationService {
|
||||
+130
-25
@@ -1,21 +1,116 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
package com.zaneschepke.wireguardautotunnel.notification
|
||||
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService.NotificationChannels
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.PROXY_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.PROXY_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.TUNNEL_ERROR_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.TUNNEL_MESSAGES_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.VPN_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.VPN_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService.NotificationChannels
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.PROXY_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.PROXY_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.TUNNEL_ERROR_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.TUNNEL_MESSAGES_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.VPN_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.VPN_NOTIFICATION_ID
|
||||
|
||||
class AndroidTunnelNotificationService(private val notificationService: NotificationService) :
|
||||
TunnelNotificationService {
|
||||
|
||||
private val context = notificationService.context
|
||||
|
||||
private fun createGroupNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>,
|
||||
channel: NotificationChannels.Tunnel,
|
||||
groupKey: String,
|
||||
): android.app.Notification {
|
||||
val title =
|
||||
if (tunnelNotificationLines.size == 1) {
|
||||
val name = tunnelNotificationLines.values.first().name
|
||||
when (channel) {
|
||||
is NotificationChannels.Tunnel.VPN ->
|
||||
"${context.getString(R.string.vpn)} • $name"
|
||||
is NotificationChannels.Tunnel.Proxy ->
|
||||
"${context.getString(R.string.proxy)} • $name"
|
||||
}
|
||||
} else {
|
||||
when (channel) {
|
||||
is NotificationChannels.Tunnel.VPN -> context.getString(R.string.vpn)
|
||||
is NotificationChannels.Tunnel.Proxy -> context.getString(R.string.proxy)
|
||||
}
|
||||
}
|
||||
|
||||
val formattedLines =
|
||||
tunnelNotificationLines.values.map { line ->
|
||||
val status = line.displayState.asLocalizedString(context)
|
||||
|
||||
if (tunnelNotificationLines.size == 1) {
|
||||
status
|
||||
} else {
|
||||
context.getString(R.string.notification_tunnel_status_format, line.name, status)
|
||||
}
|
||||
}
|
||||
val description = formattedLines.joinToString("\n")
|
||||
|
||||
val actions =
|
||||
if (tunnelNotificationLines.size == 1) {
|
||||
val tunnelId = tunnelNotificationLines.keys.first()
|
||||
listOf(
|
||||
notificationService.createNotificationAction(
|
||||
notificationAction = NotificationAction.TUNNEL_OFF,
|
||||
extraId = tunnelId,
|
||||
)
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
notificationService.createNotificationAction(
|
||||
notificationAction = NotificationAction.STOP_ALL,
|
||||
extraId = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val style =
|
||||
if (tunnelNotificationLines.size > 1) {
|
||||
NotificationCompat.InboxStyle()
|
||||
.setBigContentTitle(title)
|
||||
.setSummaryText(
|
||||
"${tunnelNotificationLines.size} ${context.getString(R.string.tunnels).lowercase()}"
|
||||
)
|
||||
.also { inbox -> formattedLines.forEach { inbox.addLine(it) } }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return notificationService.createNotification(
|
||||
channel = channel,
|
||||
title = title,
|
||||
description = description,
|
||||
actions = actions,
|
||||
onGoing = true,
|
||||
onlyAlertOnce = true,
|
||||
groupKey = groupKey,
|
||||
style = style,
|
||||
)
|
||||
}
|
||||
|
||||
override fun buildVpnPersistentNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||
): android.app.Notification {
|
||||
return createGroupNotification(
|
||||
tunnelNotificationLines,
|
||||
NotificationChannels.Tunnel.VPN,
|
||||
VPN_GROUP_KEY,
|
||||
)
|
||||
}
|
||||
|
||||
override fun buildProxyPersistentNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||
): android.app.Notification {
|
||||
return createGroupNotification(
|
||||
tunnelNotificationLines,
|
||||
NotificationChannels.Tunnel.Proxy,
|
||||
PROXY_GROUP_KEY,
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateGroupNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>,
|
||||
notificationId: Int,
|
||||
@@ -88,26 +183,36 @@ class AndroidTunnelNotificationService(private val notificationService: Notifica
|
||||
notificationService.show(notificationId, notification)
|
||||
}
|
||||
|
||||
override fun updateProxyPersistentNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||
) {
|
||||
updateGroupNotification(
|
||||
tunnelNotificationLines = tunnelNotificationLines,
|
||||
notificationId = PROXY_NOTIFICATION_ID,
|
||||
channel = NotificationChannels.Tunnel.Proxy,
|
||||
groupKey = PROXY_GROUP_KEY,
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateVpnPersistentNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||
) {
|
||||
updateGroupNotification(
|
||||
tunnelNotificationLines = tunnelNotificationLines,
|
||||
notificationId = VPN_NOTIFICATION_ID,
|
||||
channel = NotificationChannels.Tunnel.VPN,
|
||||
groupKey = VPN_GROUP_KEY,
|
||||
)
|
||||
if (tunnelNotificationLines.isEmpty()) {
|
||||
notificationService.remove(VPN_NOTIFICATION_ID)
|
||||
return
|
||||
}
|
||||
val notification =
|
||||
createGroupNotification(
|
||||
tunnelNotificationLines,
|
||||
NotificationChannels.Tunnel.VPN,
|
||||
VPN_GROUP_KEY,
|
||||
)
|
||||
notificationService.show(VPN_NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
override fun updateProxyPersistentNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||
) {
|
||||
if (tunnelNotificationLines.isEmpty()) {
|
||||
notificationService.remove(PROXY_NOTIFICATION_ID)
|
||||
return
|
||||
}
|
||||
val notification =
|
||||
createGroupNotification(
|
||||
tunnelNotificationLines,
|
||||
NotificationChannels.Tunnel.Proxy,
|
||||
PROXY_GROUP_KEY,
|
||||
)
|
||||
notificationService.show(PROXY_NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
override fun showIpv4Fallback(tunnelName: String) {
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
package com.zaneschepke.wireguardautotunnel.notification
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService.NotificationChannels
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService.NotificationChannels
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
|
||||
interface NotificationService {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
package com.zaneschepke.wireguardautotunnel.notification
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||
|
||||
+11
-1
@@ -1,4 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
package com.zaneschepke.wireguardautotunnel.notification
|
||||
|
||||
import android.app.Notification
|
||||
|
||||
interface TunnelNotificationService {
|
||||
|
||||
@@ -6,6 +8,14 @@ interface TunnelNotificationService {
|
||||
|
||||
fun updateVpnPersistentNotification(tunnelNotificationLines: Map<Int, TunnelNotificationLine>)
|
||||
|
||||
fun buildVpnPersistentNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||
): Notification
|
||||
|
||||
fun buildProxyPersistentNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||
): Notification
|
||||
|
||||
fun showIpv4Fallback(tunnelName: String)
|
||||
|
||||
fun showIpv6Recovery(tunnelName: String)
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service
|
||||
package com.zaneschepke.wireguardautotunnel.service
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
|
||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelService
|
||||
|
||||
class ServiceManager(private val context: Context) {
|
||||
|
||||
+14
-2
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||
package com.zaneschepke.wireguardautotunnel.service.autotunnel
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
@@ -28,7 +28,19 @@ class AutoTunnelEngine {
|
||||
|
||||
val activeTunnelIds = backend.activeTunnels.keys.toSet()
|
||||
|
||||
if (!network.hasInternet()) {
|
||||
val isOnCaptivePortalWifi =
|
||||
network.activeNetwork is ActiveNetwork.Wifi &&
|
||||
network.activeNetwork.requiresCaptivePortalLogin
|
||||
|
||||
if (isOnCaptivePortalWifi && settings.disableTunnelOnCaptivePortal) {
|
||||
return if (activeTunnelIds.isNotEmpty()) {
|
||||
Decision.Sync(start = emptySet(), stop = activeTunnelIds)
|
||||
} else {
|
||||
Decision.None
|
||||
}
|
||||
}
|
||||
|
||||
if (!network.hasUsableNetwork) {
|
||||
return if (settings.isStopOnNoInternetEnabled) {
|
||||
Decision.StopDueToNoInternet
|
||||
} else {
|
||||
+12
-8
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||
package com.zaneschepke.wireguardautotunnel.service.autotunnel
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.core.app.ServiceCompat
|
||||
@@ -7,10 +7,7 @@ import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.di.Dispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelActionSource
|
||||
@@ -23,17 +20,21 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepos
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.toDomain
|
||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.to
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
@@ -77,13 +78,16 @@ class AutoTunnelService : LifecycleService() {
|
||||
@Volatile private var hasUserOverride = false
|
||||
private var lastNetworkFingerprint: AutoTunnelState.NetworkFingerprint? = null
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
private val autoTunnelStateFlow: Flow<AutoTunnelState> by lazy {
|
||||
val networkFlow = networkEngine.stableState.mapNotNull { it?.state?.toDomain() }
|
||||
|
||||
val settingsFlow = combineSettings()
|
||||
|
||||
val backendFlow =
|
||||
tunnelCoordinator.backendStatus.distinctUntilChangedBy { it.activeTunnels.keys.toSet() }
|
||||
tunnelCoordinator.backendStatus
|
||||
.distinctUntilChanged { old, new -> old.activeTunnels == new.activeTunnels }
|
||||
.debounce(300L.milliseconds)
|
||||
|
||||
combine(networkFlow, settingsFlow, backendFlow) { network, settings, backend ->
|
||||
AutoTunnelState(
|
||||
@@ -188,11 +192,11 @@ class AutoTunnelService : LifecycleService() {
|
||||
reconciliationMutex.withLock {
|
||||
val currentNetworkState = networkEngine.stableState.value?.state?.toDomain()
|
||||
|
||||
val stillNoInternet = currentNetworkState?.hasInternet() == false
|
||||
val stillNoUsableNetwork = currentNetworkState?.hasUsableNetwork == false
|
||||
val stopOnNoInternetEnabled =
|
||||
autoTunnelRepository.flow.firstOrNull()?.isStopOnNoInternetEnabled == true
|
||||
|
||||
if (stillNoInternet && stopOnNoInternetEnabled) {
|
||||
if (stillNoUsableNetwork && stopOnNoInternetEnabled) {
|
||||
val currentActiveIds =
|
||||
tunnelCoordinator.backendStatus.value.activeTunnels.keys
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||
package com.zaneschepke.wireguardautotunnel.service.autotunnel
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
||||
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
||||
|
||||
import android.content.Context
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
||||
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.Tile
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.dialog
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
|
||||
@Composable
|
||||
fun LocalNetworkPermissionDialog(onDismiss: () -> Unit, onAttest: () -> Unit) {
|
||||
InfoDialog(
|
||||
onAttest = onAttest,
|
||||
onDismiss = onDismiss,
|
||||
title = stringResource(R.string.local_network_permission_title),
|
||||
body = {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.local_network_permission_intro),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.local_network_permission_issues_intro),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.local_network_permission_feature_tunnels),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.local_network_permission_feature_autotunnel),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.local_network_permission_feature_proxy),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.local_network_permission_recommendation),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.local_network_permission_nearby_devices),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmText = stringResource(R.string._continue),
|
||||
)
|
||||
}
|
||||
-104
@@ -1,104 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Favorite
|
||||
import androidx.compose.material.icons.rounded.Close
|
||||
import androidx.compose.material.icons.rounded.Info
|
||||
import androidx.compose.material.icons.rounded.Warning
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Snackbar
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
|
||||
|
||||
@Composable
|
||||
fun CustomSnackBar(
|
||||
message: AnnotatedString,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
type: SnackbarType = SnackbarType.INFO,
|
||||
containerColor: Color = MaterialTheme.colorScheme.surface,
|
||||
) {
|
||||
val isTv = LocalIsAndroidTV.current
|
||||
val icon =
|
||||
when (type) {
|
||||
SnackbarType.INFO -> Icons.Rounded.Info
|
||||
SnackbarType.WARNING -> Icons.Rounded.Warning
|
||||
SnackbarType.THANK_YOU -> Icons.Outlined.Favorite
|
||||
}
|
||||
val iconDescription =
|
||||
when (type) {
|
||||
SnackbarType.INFO -> stringResource(R.string.info)
|
||||
SnackbarType.WARNING -> stringResource(R.string.warning)
|
||||
SnackbarType.THANK_YOU -> stringResource(R.string.thank_you)
|
||||
}
|
||||
|
||||
Snackbar(
|
||||
containerColor = containerColor,
|
||||
modifier =
|
||||
modifier
|
||||
.wrapContentHeight(align = Alignment.Top)
|
||||
.padding(horizontal = if (isTv) 48.dp else 16.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.height(IntrinsicSize.Min)
|
||||
.width(IntrinsicSize.Min)
|
||||
.padding(vertical = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().weight(1f),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = iconDescription,
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Text(
|
||||
text = message,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 8,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
Row {
|
||||
IconButton(onClick = onDismiss, modifier = Modifier.size(24.dp)) {
|
||||
Icon(
|
||||
Icons.Rounded.Close,
|
||||
contentDescription = stringResource(R.string.stop),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-76
@@ -1,76 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun rememberCustomSnackbarState(): CustomSnackbarState {
|
||||
return remember { CustomSnackbarState() }
|
||||
}
|
||||
|
||||
class CustomSnackbarState {
|
||||
private val _snackbars = Channel<SnackbarInfo>(Channel.BUFFERED)
|
||||
val snackbars: Channel<SnackbarInfo> = _snackbars
|
||||
|
||||
private var currentSnackbar by mutableStateOf<SnackbarInfo?>(null)
|
||||
private var isShowing by mutableStateOf(false)
|
||||
|
||||
fun showSnackbar(info: SnackbarInfo) {
|
||||
_snackbars.trySend(info)
|
||||
}
|
||||
|
||||
fun dismissCurrent() {
|
||||
currentSnackbar = null
|
||||
isShowing = false
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SnackbarHost(
|
||||
modifier: Modifier = Modifier,
|
||||
snackbar: @Composable (SnackbarInfo) -> Unit = { info ->
|
||||
CustomSnackBar(
|
||||
message = info.message,
|
||||
type = info.type,
|
||||
onDismiss = { dismissCurrent() },
|
||||
modifier = Modifier,
|
||||
containerColor = MaterialTheme.colorScheme.surface.copy(.1f),
|
||||
)
|
||||
},
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
LaunchedEffect(Unit) {
|
||||
for (info in snackbars) {
|
||||
currentSnackbar = info
|
||||
isShowing = true
|
||||
|
||||
scope.launch {
|
||||
delay(info.durationMs)
|
||||
if (currentSnackbar?.id == info.id) {
|
||||
dismissCurrent()
|
||||
}
|
||||
}
|
||||
|
||||
while (isShowing && currentSnackbar?.id == info.id) {
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentSnackbar?.let { info ->
|
||||
if (isShowing) {
|
||||
Box(modifier = modifier) { snackbar(info) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
|
||||
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
|
||||
enum class SnackbarType {
|
||||
INFO,
|
||||
WARNING,
|
||||
THANK_YOU,
|
||||
}
|
||||
|
||||
data class SnackbarInfo(
|
||||
val message: AnnotatedString,
|
||||
val type: SnackbarType = SnackbarType.INFO,
|
||||
val durationMs: Long = 4000L,
|
||||
val id: String = System.currentTimeMillis().toString(),
|
||||
)
|
||||
+16
@@ -10,6 +10,7 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Filter1
|
||||
import androidx.compose.material.icons.outlined.Map
|
||||
import androidx.compose.material.icons.outlined.PublicOff
|
||||
import androidx.compose.material.icons.outlined.WifiFind
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -191,6 +192,21 @@ fun WifiSettingsScreen(viewModel: AutoTunnelViewModel = koinViewModel()) {
|
||||
)
|
||||
},
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.PublicOff, contentDescription = null) },
|
||||
title = stringResource(R.string.stop_while_captive_portal),
|
||||
onClick = {
|
||||
viewModel.setDisabledOnCaptivePortal(
|
||||
!uiState.autoTunnelSettings.disableTunnelOnCaptivePortal
|
||||
)
|
||||
},
|
||||
trailing = {
|
||||
ThemedSwitch(
|
||||
checked = uiState.autoTunnelSettings.disableTunnelOnCaptivePortal,
|
||||
onClick = { viewModel.setDisabledOnCaptivePortal(it) },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
Column {
|
||||
GroupLabel(stringResource(R.string.tunnels), Modifier.padding(horizontal = 16.dp))
|
||||
|
||||
+9
-2
@@ -9,6 +9,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||
@@ -45,11 +46,17 @@ fun PinLockScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
||||
textColor = MaterialTheme.colorScheme.onSurface,
|
||||
onPinCorrect = { onPinCorrect() },
|
||||
onPinIncorrect = {
|
||||
sharedViewModel.showToast(StringValue.StringResource(R.string.incorrect_pin))
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.incorrect_pin),
|
||||
ToastType.Warning,
|
||||
)
|
||||
},
|
||||
onPinCreated = {
|
||||
pinCreated = true
|
||||
sharedViewModel.showToast(StringValue.StringResource(R.string.pin_created))
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.pin_created),
|
||||
ToastType.Success,
|
||||
)
|
||||
sharedViewModel.setPinLockEnabled(true)
|
||||
onPinCorrect()
|
||||
},
|
||||
|
||||
+49
-10
@@ -38,6 +38,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
@@ -51,12 +52,12 @@ import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackupBottomSheet
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackupEncryptionDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.proxy.compoents.AppModeBottomSheet
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asString
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.capitalize
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
@@ -88,6 +89,9 @@ fun SettingsScreen(
|
||||
}
|
||||
|
||||
var showBackupSheet by rememberSaveable { mutableStateOf(false) }
|
||||
var showEncryptionDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var isRestoreAction by remember { mutableStateOf(false) }
|
||||
|
||||
var showAppModeSheet by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val appMode = uiState.settings.tunnelMode
|
||||
@@ -99,19 +103,53 @@ fun SettingsScreen(
|
||||
}
|
||||
|
||||
fun performBackupRestore(action: () -> Unit) {
|
||||
if (uiState.tunnelActive || globalUiState.isAutoTunnelActive)
|
||||
return context.showToast(R.string.all_services_disabled)
|
||||
showBackupSheet = false
|
||||
if (uiState.tunnelActive || globalUiState.isAutoTunnelActive) {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.all_services_disabled),
|
||||
ToastType.Warning,
|
||||
)
|
||||
return
|
||||
}
|
||||
action()
|
||||
}
|
||||
|
||||
if (showBackupSheet)
|
||||
if (showBackupSheet) {
|
||||
BackupBottomSheet(
|
||||
{ performBackupRestore { (context as? MainActivity)?.performBackup() } },
|
||||
{ performBackupRestore { (context as? MainActivity)?.performRestore() } },
|
||||
) {
|
||||
showBackupSheet = false
|
||||
}
|
||||
onBackup = {
|
||||
showBackupSheet = false
|
||||
isRestoreAction = false
|
||||
showEncryptionDialog = true
|
||||
},
|
||||
onRestore = {
|
||||
showBackupSheet = false
|
||||
isRestoreAction = true
|
||||
showEncryptionDialog = true
|
||||
},
|
||||
onDismiss = { showBackupSheet = false },
|
||||
)
|
||||
}
|
||||
|
||||
if (showEncryptionDialog) {
|
||||
BackupEncryptionDialog(
|
||||
isRestore = isRestoreAction,
|
||||
onConfirm = { encrypt, password ->
|
||||
showEncryptionDialog = false
|
||||
|
||||
if (isRestoreAction) {
|
||||
performBackupRestore {
|
||||
(context as? MainActivity)?.performRestore(encrypt, password)
|
||||
}
|
||||
} else {
|
||||
performBackupRestore {
|
||||
(context as? MainActivity)?.performBackup(encrypt, password)
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismiss = { showEncryptionDialog = false },
|
||||
)
|
||||
}
|
||||
|
||||
if (showAppModeSheet)
|
||||
AppModeBottomSheet(sharedViewModel::setAppMode, uiState.settings.tunnelMode) {
|
||||
showAppModeSheet = false
|
||||
@@ -168,7 +206,8 @@ fun SettingsScreen(
|
||||
StringValue.StringResource(
|
||||
R.string.mode_disabled_template,
|
||||
appMode.asString(context),
|
||||
)
|
||||
),
|
||||
ToastType.Info,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Visibility
|
||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField
|
||||
|
||||
@Composable
|
||||
fun BackupEncryptionDialog(
|
||||
isRestore: Boolean,
|
||||
onConfirm: (encrypt: Boolean, password: String?) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
var encrypt by remember { mutableStateOf(false) }
|
||||
var password by remember { mutableStateOf("") }
|
||||
var confirmPassword by remember { mutableStateOf("") }
|
||||
var showPasswordError by remember { mutableStateOf(false) }
|
||||
var passwordVisible by remember { mutableStateOf(false) }
|
||||
var confirmPasswordVisible by remember { mutableStateOf(false) }
|
||||
|
||||
InfoDialog(
|
||||
title =
|
||||
if (isRestore) {
|
||||
stringResource(R.string.restore)
|
||||
} else {
|
||||
stringResource(R.string.backup)
|
||||
},
|
||||
confirmText =
|
||||
if (isRestore) {
|
||||
stringResource(R.string.restore)
|
||||
} else {
|
||||
stringResource(R.string.backup)
|
||||
},
|
||||
onAttest = {
|
||||
if (!isRestore && encrypt && password != confirmPassword) {
|
||||
showPasswordError = true
|
||||
return@InfoDialog
|
||||
}
|
||||
if (encrypt && password.isBlank()) {
|
||||
return@InfoDialog
|
||||
}
|
||||
|
||||
val finalPassword = if (encrypt) password else null
|
||||
onConfirm(encrypt, finalPassword)
|
||||
},
|
||||
onDismiss = onDismiss,
|
||||
body = {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(stringResource(R.string.encrypted))
|
||||
ThemedSwitch(checked = encrypt, onClick = { encrypt = it })
|
||||
}
|
||||
|
||||
if (encrypt) {
|
||||
CustomTextField(
|
||||
value = password,
|
||||
onValueChange = {
|
||||
password = it
|
||||
showPasswordError = false
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
label = { Text(stringResource(R.string.password)) },
|
||||
visualTransformation =
|
||||
if (passwordVisible) VisualTransformation.None
|
||||
else PasswordVisualTransformation(),
|
||||
trailing = {
|
||||
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (passwordVisible) Icons.Outlined.VisibilityOff
|
||||
else Icons.Outlined.Visibility,
|
||||
contentDescription =
|
||||
if (passwordVisible) stringResource(R.string.hide_password)
|
||||
else stringResource(R.string.show_password),
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
if (!isRestore) {
|
||||
CustomTextField(
|
||||
value = confirmPassword,
|
||||
onValueChange = {
|
||||
confirmPassword = it
|
||||
showPasswordError = false
|
||||
},
|
||||
label = { Text(stringResource(R.string.confirm_password)) },
|
||||
visualTransformation =
|
||||
if (confirmPasswordVisible) VisualTransformation.None
|
||||
else PasswordVisualTransformation(),
|
||||
trailing = {
|
||||
IconButton(
|
||||
onClick = { confirmPasswordVisible = !confirmPasswordVisible }
|
||||
) {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (confirmPasswordVisible) Icons.Outlined.VisibilityOff
|
||||
else Icons.Outlined.Visibility,
|
||||
contentDescription =
|
||||
if (confirmPasswordVisible)
|
||||
stringResource(R.string.hide_password)
|
||||
else stringResource(R.string.show_password),
|
||||
)
|
||||
}
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isError = showPasswordError,
|
||||
)
|
||||
}
|
||||
|
||||
if (showPasswordError) {
|
||||
Text(
|
||||
text = stringResource(R.string.passwords_do_not_match),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
+5
-2
@@ -12,6 +12,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.components.LogList
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.components.LogsBottomSheet
|
||||
@@ -86,13 +87,15 @@ fun LogsScreen(
|
||||
},
|
||||
onCanceled = {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.export_canceled)
|
||||
StringValue.StringResource(R.string.export_canceled),
|
||||
ToastType.Warning,
|
||||
)
|
||||
showLogsSheet = false
|
||||
},
|
||||
onUnsupported = {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.export_unsupported)
|
||||
StringValue.StringResource(R.string.export_unsupported),
|
||||
ToastType.Warning,
|
||||
)
|
||||
showLogsSheet = false
|
||||
},
|
||||
|
||||
+67
-15
@@ -26,6 +26,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -37,8 +38,10 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
|
||||
@@ -49,12 +52,13 @@ import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.PermissionDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.UpdateDialog
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchPlayStoreListing
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchPlayStoreReview
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchSupportEmail
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SupportViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.orbitmvi.orbit.compose.collectAsState
|
||||
|
||||
@@ -63,11 +67,24 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.current
|
||||
val isTv = LocalIsAndroidTV.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val supportState by viewModel.collectAsState()
|
||||
|
||||
val clipboardManager = rememberClipboardHelper()
|
||||
|
||||
val issuesUrl = stringResource(R.string.github_url)
|
||||
val izzyUrl = stringResource(R.string.fdroid_url)
|
||||
val telegramUrl = stringResource(R.string.telegram_url)
|
||||
val matrixUrl = stringResource(R.string.matrix_url)
|
||||
val docsUrl = stringResource(R.string.docs_url)
|
||||
val websiteUrl = stringResource(R.string.website_url)
|
||||
val translationUrl = stringResource(R.string.translation_url)
|
||||
val privacyPolicyUrl = stringResource(R.string.privacy_policy_url)
|
||||
val playStoreUrl = "https://play.google.com/store/apps/details?id=${context.packageName}"
|
||||
val playReviewsUrl =
|
||||
"https://play.google.com/store/apps/details?id=${context.packageName}&showAllReviews=true"
|
||||
|
||||
val version = remember {
|
||||
"v${BuildConfig.VERSION_NAME +
|
||||
if(BuildConfig.DEBUG) "-debug" else "" }"
|
||||
@@ -95,6 +112,19 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
PermissionDialog(context = context, onDismiss = { showPermissionDialog = false })
|
||||
}
|
||||
|
||||
fun openWebUrl(url: String) {
|
||||
context.openWebUrl(url).onFailure {
|
||||
scope.launch {
|
||||
viewModel.postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.no_browser_detected),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
@@ -115,19 +145,19 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
)
|
||||
SurfaceRow(
|
||||
stringResource(R.string.docs_description),
|
||||
onClick = { context.openWebUrl(context.getString(R.string.docs_url)) },
|
||||
onClick = { openWebUrl(docsUrl) },
|
||||
leading = { Icon(Icons.Outlined.Book, contentDescription = null) },
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
)
|
||||
SurfaceRow(
|
||||
stringResource(R.string.website),
|
||||
onClick = { context.openWebUrl(context.getString(R.string.website_url)) },
|
||||
onClick = { openWebUrl(websiteUrl) },
|
||||
leading = { Icon(Icons.Outlined.Web, contentDescription = null) },
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
)
|
||||
SurfaceRow(
|
||||
stringResource(R.string.translation),
|
||||
onClick = { context.openWebUrl(context.getString(R.string.translation_url)) },
|
||||
onClick = { openWebUrl(translationUrl) },
|
||||
description = { DescriptionText(stringResource(R.string.help_translate)) },
|
||||
leading = { Icon(Icons.Outlined.Translate, contentDescription = null) },
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
@@ -141,14 +171,16 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
leading = { Icon(Icons.Outlined.Policy, contentDescription = null) },
|
||||
title = stringResource(R.string.privacy_policy),
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
onClick = { context.openWebUrl(context.getString(R.string.privacy_policy_url)) },
|
||||
onClick = { openWebUrl(privacyPolicyUrl) },
|
||||
)
|
||||
if (BuildConfig.FLAVOR == Constants.GOOGLE_PLAY_FLAVOR) {
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.Reviews, contentDescription = null) },
|
||||
title = stringResource(R.string.review),
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
onClick = { context.launchPlayStoreReview() },
|
||||
onClick = {
|
||||
context.launchPlayStoreReview().onFailure { openWebUrl(playReviewsUrl) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -167,7 +199,7 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
},
|
||||
title = stringResource(R.string.join_matrix),
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
onClick = { context.openWebUrl(context.getString(R.string.matrix_url)) },
|
||||
onClick = { openWebUrl(matrixUrl) },
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = {
|
||||
@@ -179,7 +211,7 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
},
|
||||
title = stringResource(R.string.join_telegram),
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
onClick = { context.openWebUrl(context.getString(R.string.telegram_url)) },
|
||||
onClick = { openWebUrl(telegramUrl) },
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = {
|
||||
@@ -191,13 +223,24 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
},
|
||||
title = stringResource(R.string.open_issue),
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
onClick = { context.openWebUrl(context.getString(R.string.github_url)) },
|
||||
onClick = { openWebUrl(issuesUrl) },
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.Mail, contentDescription = null) },
|
||||
title = stringResource(R.string.email_description),
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
onClick = { context.launchSupportEmail() },
|
||||
onClick = {
|
||||
context.launchSupportEmail().onFailure {
|
||||
scope.launch {
|
||||
viewModel.postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.no_email_detected),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
Column {
|
||||
@@ -222,12 +265,21 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
leading = { Icon(Icons.Outlined.InstallMobile, contentDescription = null) },
|
||||
title = stringResource(R.string.check_for_update),
|
||||
onClick = {
|
||||
if (BuildConfig.DEBUG)
|
||||
return@SurfaceRow context.showToast(R.string.update_check_unsupported)
|
||||
if (BuildConfig.DEBUG) {
|
||||
scope.launch {
|
||||
viewModel.postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.update_check_unsupported),
|
||||
ToastType.Warning,
|
||||
)
|
||||
)
|
||||
}
|
||||
return@SurfaceRow
|
||||
}
|
||||
when (BuildConfig.FLAVOR) {
|
||||
Constants.GOOGLE_PLAY_FLAVOR -> context.launchPlayStoreListing()
|
||||
Constants.FDROID_FLAVOR ->
|
||||
context.openWebUrl(context.getString(R.string.fdroid_url))
|
||||
Constants.GOOGLE_PLAY_FLAVOR ->
|
||||
context.launchPlayStoreListing().onFailure { openWebUrl(playStoreUrl) }
|
||||
Constants.FDROID_FLAVOR -> openWebUrl(izzyUrl)
|
||||
else -> viewModel.checkForStandaloneUpdate()
|
||||
}
|
||||
},
|
||||
|
||||
+17
-7
@@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||
@@ -50,11 +51,15 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
||||
rememberFileExportLauncherForResult(
|
||||
onSuccess = { uri -> sharedViewModel.exportSelectedTunnels(uri) },
|
||||
onCanceled = {
|
||||
sharedViewModel.showToast(StringValue.StringResource(R.string.export_canceled))
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.export_canceled),
|
||||
ToastType.Warning,
|
||||
)
|
||||
},
|
||||
onUnsupported = {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.export_unsupported)
|
||||
StringValue.StringResource(R.string.export_unsupported),
|
||||
ToastType.Warning,
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -73,7 +78,8 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
||||
selectedTunnelsExportLauncher.launch(fileName)
|
||||
} else {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.error_no_file_explorer)
|
||||
StringValue.StringResource(R.string.error_no_file_explorer),
|
||||
ToastType.Error,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -87,7 +93,8 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
||||
rememberFileImportLauncherForResult(
|
||||
onNoFileExplorer = {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.error_no_file_explorer)
|
||||
StringValue.StringResource(R.string.error_no_file_explorer),
|
||||
ToastType.Error,
|
||||
)
|
||||
},
|
||||
onData = { data -> sharedViewModel.importFromUri(data) },
|
||||
@@ -101,13 +108,15 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
||||
}
|
||||
QRResult.QRMissingPermission -> {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.camera_permission_required)
|
||||
StringValue.StringResource(R.string.camera_permission_required),
|
||||
ToastType.Warning,
|
||||
)
|
||||
}
|
||||
is QRResult.QRSuccess -> {
|
||||
result.content.rawValue?.let { sharedViewModel.importFromQr(it) }
|
||||
?: sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.config_error)
|
||||
StringValue.StringResource(R.string.config_error),
|
||||
ToastType.Error,
|
||||
)
|
||||
}
|
||||
QRResult.QRUserCanceled -> Unit
|
||||
@@ -119,7 +128,8 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
||||
->
|
||||
if (!isGranted) {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.camera_permission_required)
|
||||
StringValue.StringResource(R.string.camera_permission_required),
|
||||
ToastType.Warning,
|
||||
)
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
|
||||
+1
-1
@@ -42,6 +42,6 @@ private fun TransferMetric(icon: ImageVector, text: String, style: TextStyle, co
|
||||
modifier = Modifier.size(12.dp),
|
||||
)
|
||||
|
||||
Text(text = text.lowercase(), style = style, color = color)
|
||||
Text(text = text, style = style, color = color)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-15
@@ -17,7 +17,6 @@ import androidx.compose.material3.scrollbar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -38,8 +37,6 @@ import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.TunnelsUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
@@ -52,14 +49,6 @@ fun TunnelList(
|
||||
val context = LocalContext.current
|
||||
val isTv = LocalIsAndroidTV.current
|
||||
|
||||
val now by
|
||||
produceState(System.currentTimeMillis()) {
|
||||
while (true) {
|
||||
delay(1_000L.milliseconds)
|
||||
value = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
@@ -105,10 +94,7 @@ fun TunnelList(
|
||||
uiState.backendStatus.activeTunnels[tunnel.id] ?: ActiveTunnel()
|
||||
}
|
||||
|
||||
val displayState =
|
||||
remember(activeTunnel, now, uiState.displayStates[tunnel.id]) {
|
||||
uiState.displayStates[tunnel.id] ?: DisplayTunnelState.from(activeTunnel, now)
|
||||
}
|
||||
val displayState = remember(activeTunnel) { DisplayTunnelState.from(activeTunnel) }
|
||||
|
||||
val isRunning = uiState.backendStatus.activeTunnels.containsKey(tunnel.id)
|
||||
|
||||
|
||||
+9
-2
@@ -28,14 +28,16 @@ import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.config.components.QrCodeDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.ConfigHeaderColor
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.ConfigKeyColor
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isTextTooLargeForQr
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
|
||||
import org.koin.compose.viewmodel.koinActivityViewModel
|
||||
@@ -76,7 +78,12 @@ fun ConfigScreen(
|
||||
when (sideEffect) {
|
||||
is LocalSideEffect.Modal.QR -> {
|
||||
if (tunnel.quickConfig.isTextTooLargeForQr()) {
|
||||
context.showToast(R.string.text_too_large_for_qr)
|
||||
sharedViewModel.postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.text_too_large_for_qr),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
} else {
|
||||
showQrModal = true
|
||||
}
|
||||
|
||||
+23
-63
@@ -26,97 +26,57 @@ sealed class DisplayTunnelState {
|
||||
|
||||
data object Connected : DisplayTunnelState()
|
||||
|
||||
data object Degraded : DisplayTunnelState()
|
||||
data object HandshakeFailure : DisplayTunnelState()
|
||||
|
||||
@StringRes
|
||||
fun labelRes(): Int {
|
||||
return when (this) {
|
||||
fun labelRes(): Int =
|
||||
when (this) {
|
||||
Disconnected -> R.string.tunnel_state_disconnected
|
||||
Connecting -> R.string.tunnel_state_starting
|
||||
ResolvingDns -> R.string.tunnel_state_resolving_dns
|
||||
Connecting,
|
||||
EstablishingConnection -> R.string.tunnel_state_establishing_connection
|
||||
ResolvingDns -> R.string.tunnel_state_resolving_dns
|
||||
Ready -> R.string.ready
|
||||
Connected -> R.string.tunnel_state_connected
|
||||
Degraded -> R.string.tunnel_state_handshake_failure
|
||||
HandshakeFailure -> R.string.tunnel_state_handshake_failure
|
||||
}
|
||||
|
||||
fun asColor(): Color =
|
||||
when (this) {
|
||||
Disconnected -> CoolGray
|
||||
Connecting,
|
||||
ResolvingDns,
|
||||
EstablishingConnection,
|
||||
Ready -> Straw
|
||||
Connected -> SilverTree
|
||||
HandshakeFailure -> AlertRed
|
||||
}
|
||||
}
|
||||
|
||||
fun asLocalizedString(context: Context): String {
|
||||
return context.getString(labelRes())
|
||||
}
|
||||
|
||||
fun asColor(): Color {
|
||||
return when (this) {
|
||||
Disconnected -> CoolGray
|
||||
|
||||
Connecting,
|
||||
ResolvingDns,
|
||||
EstablishingConnection,
|
||||
Ready -> Straw
|
||||
|
||||
Connected -> SilverTree
|
||||
|
||||
Degraded -> AlertRed
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val HANDSHAKE_FAILURE_DEGRADED_THRESHOLD_MS = 6_000L
|
||||
|
||||
// During this window we avoid showing Degraded even if we see HandshakeFailure
|
||||
private const val POST_RESOLUTION_GRACE_PERIOD_MS = 3_500L
|
||||
|
||||
fun from(activeTunnel: ActiveTunnel, now: Long): DisplayTunnelState {
|
||||
fun from(activeTunnel: ActiveTunnel): DisplayTunnelState {
|
||||
val transport = activeTunnel.transportState
|
||||
val bootstrap = activeTunnel.bootstrapState
|
||||
val mode = activeTunnel.mode
|
||||
val isVpnStyle = mode is BackendMode.Vpn || mode is BackendMode.Proxy.KillSwitchPrimary
|
||||
|
||||
val bootstrapPhaseDone =
|
||||
bootstrap is BootstrapState.Complete || bootstrap is BootstrapState.None
|
||||
|
||||
// Check if we recently completed peer resolution
|
||||
val recentlyResolvedPeers =
|
||||
activeTunnel.lastPeerUpdateMs > 0 &&
|
||||
(now - activeTunnel.lastPeerUpdateMs) < POST_RESOLUTION_GRACE_PERIOD_MS
|
||||
|
||||
return when {
|
||||
transport is Tunnel.State.Down -> Disconnected
|
||||
|
||||
bootstrap is BootstrapState.Failed -> Degraded
|
||||
bootstrap is BootstrapState.Failed -> HandshakeFailure
|
||||
|
||||
bootstrap is BootstrapState.ResolvingDns ||
|
||||
bootstrap is BootstrapState.UpdatingPeers -> ResolvingDns
|
||||
|
||||
transport is Tunnel.State.Up.Healthy -> Connected
|
||||
|
||||
transport is Tunnel.State.Up.HandshakeFailure -> {
|
||||
val age = now - activeTunnel.lastStateChangeMs
|
||||
transport is Tunnel.State.Up.HandshakeFailure -> HandshakeFailure
|
||||
|
||||
if (recentlyResolvedPeers && bootstrapPhaseDone) {
|
||||
if (isVpnStyle) EstablishingConnection else Ready
|
||||
} else if (
|
||||
age > HANDSHAKE_FAILURE_DEGRADED_THRESHOLD_MS && bootstrapPhaseDone
|
||||
) {
|
||||
Degraded
|
||||
} else if (isVpnStyle && bootstrapPhaseDone) {
|
||||
EstablishingConnection
|
||||
} else if (bootstrapPhaseDone) {
|
||||
Ready
|
||||
} else {
|
||||
Connecting
|
||||
}
|
||||
}
|
||||
transport is Tunnel.State.Starting ->
|
||||
if (isVpnStyle) EstablishingConnection else Ready
|
||||
|
||||
transport is Tunnel.State.Starting -> {
|
||||
when {
|
||||
bootstrapPhaseDone -> if (isVpnStyle) EstablishingConnection else Ready
|
||||
else -> Connecting
|
||||
}
|
||||
}
|
||||
|
||||
bootstrapPhaseDone -> if (isVpnStyle) EstablishingConnection else Ready
|
||||
else -> Connecting
|
||||
else -> if (isVpnStyle) EstablishingConnection else Ready
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,5 +18,6 @@ data class GlobalAppUiState(
|
||||
val selectedTunnelCount: Int = 0,
|
||||
val alreadyDonated: Boolean = false,
|
||||
val isPinVerified: Boolean = false,
|
||||
val pendingWgImportUrl: String? = null,
|
||||
val isScreenRecordingProtectionEnabled: Boolean = false,
|
||||
)
|
||||
|
||||
@@ -16,14 +16,14 @@ val ElectricTeal = Color(0xFF4DD0E1)
|
||||
// Status colors
|
||||
val SilverTree = Color(0xFF6DB58B)
|
||||
val AlertRed = Color(0xFFCF6679)
|
||||
|
||||
val Straw = Color(0xFFD4C483)
|
||||
|
||||
val Disabled = CoolGray.copy(alpha = 0.4f)
|
||||
|
||||
// Config colors
|
||||
// Other colors
|
||||
val ConfigHeaderColor = Color(0xFFBB86FC)
|
||||
val ConfigKeyColor = Color(0xFF03DAC5)
|
||||
val Heart = Color(0xFFDB61A2)
|
||||
|
||||
sealed class ThemeColors(
|
||||
val background: Color,
|
||||
|
||||
@@ -6,15 +6,11 @@ object Constants {
|
||||
const val BASE_LOG_FILE_NAME = "wg_tunnel_logs"
|
||||
|
||||
const val VPN_SETTINGS_PACKAGE = "android.net.vpn.SETTINGS"
|
||||
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1 shl 10
|
||||
const val SPECIAL_USE_SERVICE_TYPE_ID = 1 shl 30
|
||||
|
||||
const val QR_CODE_NAME_PROPERTY = "# Name ="
|
||||
|
||||
const val FDROID_FLAVOR = "fdroid"
|
||||
const val GOOGLE_PLAY_FLAVOR = "google"
|
||||
const val STANDALONE_FLAVOR = "standalone"
|
||||
const val RELEASE = "release"
|
||||
|
||||
const val BASE_RELEASE_URL = "https://github.com/wgtunnel/wgtunnel/releases/tag/"
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ object DnsValidator {
|
||||
return Result.Valid
|
||||
}
|
||||
|
||||
private fun validateUdp(value: String): DnsValidator.Result {
|
||||
private fun validateUdp(value: String): Result {
|
||||
val parts = value.split(":")
|
||||
|
||||
val host = parts.getOrNull(0)?.trim()
|
||||
@@ -93,14 +93,14 @@ object DnsValidator {
|
||||
|
||||
// basic IP/hostname sanity check
|
||||
if (!isValidHostOrIp(host)) {
|
||||
return DnsValidator.Result.Invalid(DnsError.InvalidIpOrHost)
|
||||
return Result.Invalid(DnsError.InvalidIpOrHost)
|
||||
}
|
||||
|
||||
if (port !in 1..65535) {
|
||||
return DnsValidator.Result.Invalid(DnsError.InvalidPort)
|
||||
return Result.Invalid(DnsError.InvalidPort)
|
||||
}
|
||||
|
||||
return DnsValidator.Result.Valid
|
||||
return Result.Valid
|
||||
}
|
||||
|
||||
private fun isValidHostOrIp(value: String): Boolean {
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util
|
||||
|
||||
import com.vdurmont.semver4j.Semver
|
||||
import java.math.BigDecimal
|
||||
import kotlin.math.pow
|
||||
import timber.log.Timber
|
||||
|
||||
object NumberUtils {
|
||||
private const val BYTES_IN_KB = 1024.0
|
||||
private val BYTES_IN_MB = BYTES_IN_KB.pow(2.0)
|
||||
private val keyValidationRegex = """^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=${'$'}""".toRegex()
|
||||
|
||||
fun bytesToMB(bytes: Long): BigDecimal {
|
||||
return bytes.toBigDecimal().divide(BYTES_IN_MB.toBigDecimal())
|
||||
}
|
||||
|
||||
fun isValidKey(key: String): Boolean {
|
||||
return key.matches(keyValidationRegex)
|
||||
}
|
||||
|
||||
fun generateRandomTunnelName(): String {
|
||||
return "tunnel${randomFive()}"
|
||||
|
||||
+14
-108
@@ -1,27 +1,17 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util.extensions
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Context.POWER_SERVICE
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import android.service.quicksettings.TileService
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.net.toUri
|
||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.state.TunnelApp
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import java.io.File
|
||||
@@ -30,22 +20,11 @@ import java.util.Locale
|
||||
import kotlin.system.exitProcess
|
||||
import timber.log.Timber
|
||||
|
||||
fun Context.openWebUrl(url: String): Result<Unit> {
|
||||
return kotlin
|
||||
.runCatching {
|
||||
val webpage: Uri = url.toUri()
|
||||
val intent =
|
||||
Intent(Intent.ACTION_VIEW, webpage).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
.onFailure { showToast(R.string.no_browser_detected) }
|
||||
}
|
||||
|
||||
fun Context.isBatteryOptimizationsDisabled(): Boolean {
|
||||
val pm = getSystemService(POWER_SERVICE) as PowerManager
|
||||
return pm.isIgnoringBatteryOptimizations(packageName)
|
||||
fun Context.openWebUrl(url: String): Result<Unit> = runCatching {
|
||||
val webpage: Uri = url.toUri()
|
||||
val intent =
|
||||
Intent(Intent.ACTION_VIEW, webpage).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
fun Context.launchNotificationSettings() {
|
||||
@@ -94,30 +73,7 @@ fun Context.hasSAFSupport(mimeType: String): Boolean {
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.launchShareFile(file: File) {
|
||||
FileProvider.getUriForFile(this, getString(R.string.provider), file)
|
||||
val shareIntent =
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
type = FileUtils.ALL_FILE_TYPES
|
||||
putExtra(Intent.EXTRA_STREAM, file)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
val chooserIntent =
|
||||
Intent.createChooser(shareIntent, "").apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
|
||||
this.startActivity(chooserIntent)
|
||||
}
|
||||
|
||||
fun Context.showToast(resId: Int) {
|
||||
Toast.makeText(this, this.getString(resId), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
fun Context.showToast(message: String) {
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
fun Context.launchSupportEmail() {
|
||||
fun Context.launchSupportEmail(): Result<Unit> = runCatching {
|
||||
val intent =
|
||||
Intent(Intent.ACTION_SENDTO).apply {
|
||||
data = "mailto:".toUri()
|
||||
@@ -132,7 +88,7 @@ fun Context.launchSupportEmail() {
|
||||
}
|
||||
)
|
||||
} else {
|
||||
showToast(R.string.no_email_detected)
|
||||
throw IllegalStateException("No email client found")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +99,7 @@ fun Context.isRunningOnTv(): Boolean {
|
||||
fun Context.launchVpnSettings(): Result<Unit> {
|
||||
return kotlin.runCatching {
|
||||
val intent =
|
||||
Intent(Constants.VPN_SETTINGS_PACKAGE).apply { setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
|
||||
Intent(Constants.VPN_SETTINGS_PACKAGE).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK }
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
@@ -162,14 +118,6 @@ fun Context.launchLocationServicesSettings(): Result<Unit> {
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.launchSettings(): Result<Unit> {
|
||||
return kotlin.runCatching {
|
||||
val intent =
|
||||
Intent(Settings.ACTION_SETTINGS).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK }
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.launchAppSettings() {
|
||||
kotlin
|
||||
.runCatching {
|
||||
@@ -186,48 +134,6 @@ fun Context.launchAppSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.requestTunnelTileServiceStateUpdate() =
|
||||
runCatching {
|
||||
TileService.requestListeningState(
|
||||
this,
|
||||
ComponentName(this, TunnelControlTile::class.java),
|
||||
)
|
||||
}
|
||||
.onFailure { Timber.w(it) }
|
||||
|
||||
fun Context.requestAutoTunnelTileServiceUpdate() =
|
||||
runCatching {
|
||||
TileService.requestListeningState(
|
||||
this,
|
||||
ComponentName(this, AutoTunnelControlTile::class.java),
|
||||
)
|
||||
}
|
||||
.onFailure { Timber.w(it) }
|
||||
|
||||
fun Context.getAllInternetCapablePackages(): List<PackageInfo> {
|
||||
val permissions = arrayOf(Manifest.permission.INTERNET)
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
packageManager.getPackagesHoldingPermissions(
|
||||
permissions,
|
||||
PackageManager.PackageInfoFlags.of(0L),
|
||||
)
|
||||
} else {
|
||||
packageManager.getPackagesHoldingPermissions(permissions, 0)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.getSplitTunnelApps(): List<TunnelApp> {
|
||||
val packages = getAllInternetCapablePackages()
|
||||
return packages
|
||||
.filter { it.applicationInfo != null }
|
||||
.map { pkg ->
|
||||
TunnelApp(
|
||||
packageManager.getApplicationLabel(pkg.applicationInfo!!).toString(),
|
||||
pkg.packageName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.canInstallPackages(): Boolean {
|
||||
return packageManager.canRequestPackageInstalls()
|
||||
}
|
||||
@@ -252,9 +158,9 @@ fun Context.installApk(apkFile: File) {
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
fun Context.launchPlayStoreListing() {
|
||||
fun Context.launchPlayStoreListing(): Result<Unit> = runCatching {
|
||||
val intent =
|
||||
Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$packageName")).apply {
|
||||
Intent(Intent.ACTION_VIEW, "market://details?id=$packageName".toUri()).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
setPackage("com.android.vending")
|
||||
}
|
||||
@@ -262,12 +168,12 @@ fun Context.launchPlayStoreListing() {
|
||||
if (intent.resolveActivity(packageManager) != null) {
|
||||
startActivity(intent)
|
||||
} else {
|
||||
openWebUrl("https://play.google.com/store/apps/details?id=$packageName")
|
||||
throw IllegalStateException("Play Store not found")
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.launchPlayStoreReview() {
|
||||
val uri = Uri.parse("market://details?id=$packageName&showAllReviews=true")
|
||||
fun Context.launchPlayStoreReview(): Result<Unit> = runCatching {
|
||||
val uri = "market://details?id=$packageName&showAllReviews=true".toUri()
|
||||
val intent =
|
||||
Intent(Intent.ACTION_VIEW, uri).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
@@ -276,7 +182,7 @@ fun Context.launchPlayStoreReview() {
|
||||
if (intent.resolveActivity(packageManager) != null) {
|
||||
startActivity(intent)
|
||||
} else {
|
||||
openWebUrl("https://play.google.com/store/apps/details?id=$packageName&showAllReviews=true")
|
||||
throw IllegalStateException("Play Store not found")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util.extensions
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
fun <K, V> Flow<Map<K, V>>.distinctByKeys(): Flow<Map<K, V>> {
|
||||
return distinctUntilChanged { old, new -> old.keys == new.keys }
|
||||
}
|
||||
@@ -18,10 +18,6 @@ fun <T, R : Comparable<R>> List<T>.isSortedBy(selector: (T) -> R): Boolean {
|
||||
return zipWithNext().all { (a, b) -> selector(a) <= selector(b) }
|
||||
}
|
||||
|
||||
fun Int.toMillis(): Long {
|
||||
return this * 1_000L
|
||||
}
|
||||
|
||||
fun Double.round(decimals: Int): Double {
|
||||
val factor = 10.0.pow(decimals)
|
||||
return (this * factor).roundToInt() / factor
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util.extensions
|
||||
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
|
||||
suspend fun HttpResponse.isHtmlResponse(): Boolean {
|
||||
val contentType = headers["Content-Type"] ?: ""
|
||||
if (contentType.contains("text/html", ignoreCase = true)) return true
|
||||
|
||||
val bodyStart = bodyAsText().trimStart()
|
||||
return bodyStart.startsWith("<!DOCTYPE", ignoreCase = true) ||
|
||||
bodyStart.startsWith("<html", ignoreCase = true)
|
||||
}
|
||||
+1
-1
@@ -124,7 +124,7 @@ fun DnsError.labelRes(): Int {
|
||||
fun ActiveTunnel.statusText(context: Context): String {
|
||||
return context.getString(
|
||||
R.string.status_template,
|
||||
DisplayTunnelState.from(this, System.currentTimeMillis()).asLocalizedString(context),
|
||||
DisplayTunnelState.from(this).asLocalizedString(context),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util.permission
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
object LocalNetworkPermissionHelper {
|
||||
|
||||
fun shouldRequestPermission(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN
|
||||
}
|
||||
|
||||
fun isPermissionGranted(context: Context): Boolean {
|
||||
return if (shouldRequestPermission()) {
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_LOCAL_NETWORK) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
-10
@@ -2,13 +2,12 @@ package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
||||
import com.zaneschepke.tunnel.backend.RootShell
|
||||
import com.zaneschepke.tunnel.util.RootShell
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.WifiDetectionMethod
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
@@ -16,6 +15,8 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsR
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.AutoTunnelUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@@ -99,7 +100,10 @@ class AutoTunnelViewModel(
|
||||
val trimmed = name.trim()
|
||||
if (state.autoTunnelSettings.trustedNetworkSSIDs.contains(name)) {
|
||||
return@intent postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.error_ssid_exists))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.error_ssid_exists),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
setTrustedNetworkNames(
|
||||
@@ -141,6 +145,10 @@ class AutoTunnelViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
fun setDisabledOnCaptivePortal(enabled: Boolean) = intent {
|
||||
autoTunnelRepository.updateDisableOnCaptivePortal(enabled)
|
||||
}
|
||||
|
||||
fun removeTunnelNetwork(tunnel: TunnelConfig, ssid: String) = intent {
|
||||
tunnelsRepository.save(
|
||||
tunnel.copy(
|
||||
@@ -153,11 +161,19 @@ class AutoTunnelViewModel(
|
||||
when (method) {
|
||||
WifiDetectionMethod.ROOT -> {
|
||||
val accepted = RootShell.requestRootPermission()
|
||||
val message =
|
||||
if (!accepted) StringValue.StringResource(R.string.error_root_denied)
|
||||
else StringValue.StringResource(R.string.root_accepted)
|
||||
postSideEffect(GlobalSideEffect.Snackbar(message))
|
||||
if (!accepted) return@intent
|
||||
if (!accepted)
|
||||
return@intent postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.error_root_denied),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.root_accepted),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
}
|
||||
WifiDetectionMethod.SHIZUKU -> {
|
||||
requestShizuku()
|
||||
@@ -188,7 +204,10 @@ class AutoTunnelViewModel(
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.shizuku_not_detected))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.shizuku_not_detected),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+13
-5
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.MimicMode
|
||||
@@ -105,7 +106,10 @@ class ConfigEditViewModel(
|
||||
if (state.isTunnelNameTaken) {
|
||||
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.tunnel_name_taken))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.tunnel_name_taken),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
|
||||
return@intent
|
||||
@@ -113,7 +117,10 @@ class ConfigEditViewModel(
|
||||
|
||||
if (state.draft.tunnelName.isBlank()) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.name_error_empty))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.name_error_empty),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
return@intent
|
||||
}
|
||||
@@ -147,8 +154,9 @@ class ConfigEditViewModel(
|
||||
}
|
||||
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(
|
||||
StringValue.StringResource(R.string.config_changes_saved)
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.config_changes_saved),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -164,7 +172,7 @@ class ConfigEditViewModel(
|
||||
else -> StringValue.StringResource(R.string.unknown_error)
|
||||
}
|
||||
|
||||
postSideEffect(GlobalSideEffect.Snackbar(message))
|
||||
postSideEffect(GlobalSideEffect.Snackbar(message, ToastType.Error))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.DnsSettingsCoordinator
|
||||
@@ -9,7 +10,6 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsReposito
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarType
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.DnsUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.DnsValidator
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
@@ -70,7 +70,7 @@ class DnsViewModel(
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(result.error.labelRes()),
|
||||
type = SnackbarType.WARNING,
|
||||
type = ToastType.Error,
|
||||
)
|
||||
)
|
||||
return@intent
|
||||
@@ -87,7 +87,10 @@ class DnsViewModel(
|
||||
|
||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(StringValue.StringResource(R.string.config_changes_saved))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.config_changes_saved),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
@@ -52,7 +53,10 @@ class LockdownViewModel(
|
||||
|
||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(StringValue.StringResource(R.string.config_changes_saved))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.config_changes_saved),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.logcatter.LogReader
|
||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
@@ -61,7 +62,10 @@ class LoggerViewModel(
|
||||
fun exportLogs(uri: Uri?) = intent {
|
||||
if (uri == null) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(StringValue.StringResource(R.string.export_unsupported))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.export_unsupported),
|
||||
ToastType.Warning,
|
||||
)
|
||||
)
|
||||
return@intent
|
||||
}
|
||||
@@ -76,11 +80,12 @@ class LoggerViewModel(
|
||||
Timber.e(action)
|
||||
intent {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(
|
||||
R.string.export_failed,
|
||||
": ${action.localizedMessage}",
|
||||
)
|
||||
),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
+11
-3
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
@@ -79,7 +80,8 @@ class ProxySettingsViewModel(
|
||||
if (socksPort == null || httpPort == null || socksPort == httpPort) {
|
||||
return@intent postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.ports_must_differ)
|
||||
StringValue.StringResource(R.string.ports_must_differ),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -96,7 +98,10 @@ class ProxySettingsViewModel(
|
||||
|
||||
if (updated.proxyPassword?.any { it.isWhitespace() } == true) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.password_no_spaces))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.password_no_spaces),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
return@intent reduce { state.copy(isPasswordError = true) }
|
||||
}
|
||||
@@ -104,7 +109,10 @@ class ProxySettingsViewModel(
|
||||
proxySettingsRepository.upsert(updated)
|
||||
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.config_changes_saved))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.config_changes_saved),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||
}
|
||||
|
||||
+15
-6
@@ -1,7 +1,8 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.zaneschepke.tunnel.backend.RootShell
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.tunnel.util.RootShell
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
||||
@@ -96,11 +97,19 @@ class SettingsViewModel(
|
||||
fun setTunnelScriptedEnabled(to: Boolean) = intent {
|
||||
if (to) {
|
||||
val accepted = RootShell.requestRootPermission()
|
||||
val message =
|
||||
if (!accepted) StringValue.StringResource(R.string.error_root_denied)
|
||||
else StringValue.StringResource(R.string.root_accepted)
|
||||
postSideEffect(GlobalSideEffect.Snackbar(message))
|
||||
if (!accepted) return@intent
|
||||
if (!accepted)
|
||||
return@intent postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.error_root_denied),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.root_accepted),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
}
|
||||
settingsRepository.upsert(state.settings.copy(tunnelScriptingEnabled = to))
|
||||
}
|
||||
|
||||
+62
-30
@@ -3,11 +3,10 @@ package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelModeCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
||||
@@ -17,6 +16,8 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.SelectedTunnelsRepo
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.parser.ConfigParseException
|
||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.GlobalAppUiState
|
||||
@@ -28,6 +29,7 @@ import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.QuickConfig
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelName
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asStringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isHtmlResponse
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.saveTunnelsUniquely
|
||||
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
|
||||
import io.ktor.client.HttpClient
|
||||
@@ -77,7 +79,7 @@ class SharedAppViewModel(
|
||||
|
||||
val displayStates =
|
||||
backendStatus.activeTunnels.mapValues { (_, activeTunnel) ->
|
||||
DisplayTunnelState.from(activeTunnel, System.currentTimeMillis())
|
||||
DisplayTunnelState.from(activeTunnel)
|
||||
}
|
||||
|
||||
TunnelsUiState(
|
||||
@@ -177,6 +179,10 @@ class SharedAppViewModel(
|
||||
appStateRepository.setShouldShowDonationSnackbar(to)
|
||||
}
|
||||
|
||||
fun showSnackMessage(message: StringValue, type: ToastType) = intent {
|
||||
postGlobalSideEffect(GlobalSideEffect.Snackbar(message, type))
|
||||
}
|
||||
|
||||
suspend fun postSideEffect(globalSideEffect: GlobalSideEffect) {
|
||||
globalEffectRepository.post(globalSideEffect)
|
||||
}
|
||||
@@ -187,12 +193,6 @@ class SharedAppViewModel(
|
||||
globalEffectRepository.post(sideEffect)
|
||||
}
|
||||
|
||||
fun showSnackMessage(message: StringValue) = intent {
|
||||
postGlobalSideEffect(GlobalSideEffect.Snackbar(message))
|
||||
}
|
||||
|
||||
fun showToast(message: StringValue) = intent { postSideEffect(GlobalSideEffect.Toast(message)) }
|
||||
|
||||
fun disableBatteryOptimizationsShown() = intent {
|
||||
appStateRepository.setBatteryOptimizationDisableShown(true)
|
||||
}
|
||||
@@ -200,14 +200,20 @@ class SharedAppViewModel(
|
||||
fun saveSortChanges(tunnels: List<TunnelConfig>) = intent {
|
||||
tunnelRepository.saveAll(tunnels.mapIndexed { index, conf -> conf.copy(position = index) })
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.config_changes_saved))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.config_changes_saved),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||
}
|
||||
|
||||
fun sortByLatency(tunnels: List<TunnelConfig>) = intent {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.pinging_servers))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.pinging_servers),
|
||||
ToastType.Info,
|
||||
)
|
||||
)
|
||||
val sortedResult =
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -249,12 +255,17 @@ class SharedAppViewModel(
|
||||
TunnelConfig.tunnelConfFromQuick(config, name)
|
||||
}
|
||||
tunnelRepository.saveTunnelsUniquely(tunnelConfigs, state.tunnelNames.map { it.value })
|
||||
} catch (_: IOException) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.read_failed))
|
||||
)
|
||||
} catch (e: ConfigParseException) {
|
||||
postSideEffect(GlobalSideEffect.Snackbar(e.asStringValue()))
|
||||
} catch (e: Exception) {
|
||||
if (e is ConfigParseException) {
|
||||
postSideEffect(GlobalSideEffect.Snackbar(e.asStringValue(), ToastType.Error))
|
||||
} else {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.config_error),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,22 +275,38 @@ class SharedAppViewModel(
|
||||
|
||||
fun importFromQr(conf: String) = intent { importFromClipboard(conf) }
|
||||
|
||||
fun promptWgImport(url: String) = intent { reduce { state.copy(pendingWgImportUrl = url) } }
|
||||
|
||||
fun dismissWgImport() = intent { reduce { state.copy(pendingWgImportUrl = null) } }
|
||||
|
||||
fun importFromUrl(url: String) = intent {
|
||||
reduce { state.copy(pendingWgImportUrl = null) }
|
||||
|
||||
try {
|
||||
httpClient.prepareGet(url).execute { response ->
|
||||
if (response.status.value in 200..299) {
|
||||
val body = response.bodyAsText()
|
||||
importFromClipboard(body)
|
||||
} else {
|
||||
throw IOException(
|
||||
"Failed to download file with error status: ${response.status.value}"
|
||||
)
|
||||
if (response.status.value !in 200..299) {
|
||||
throw IOException("Server returned error: ${response.status.value}")
|
||||
}
|
||||
|
||||
if (response.isHtmlResponse()) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.error_invalid_config_url),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
return@execute
|
||||
}
|
||||
val body = response.bodyAsText()
|
||||
importFromClipboard(body)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(StringValue.StringResource(R.string.error_download_failed))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.error_download_failed),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -294,7 +321,7 @@ class SharedAppViewModel(
|
||||
is IOException -> StringValue.StringResource(R.string.error_download_failed)
|
||||
else -> StringValue.StringResource(R.string.error_file_extension)
|
||||
}
|
||||
postSideEffect(GlobalSideEffect.Toast(message))
|
||||
postSideEffect(GlobalSideEffect.Snackbar(message, ToastType.Error))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,7 +353,8 @@ class SharedAppViewModel(
|
||||
if (selectedTuns.any { activeTunIds?.contains(it.id) == true })
|
||||
return@intent postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.delete_active_message)
|
||||
StringValue.StringResource(R.string.delete_active_message),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
tunnelRepository.delete(selectedTuns)
|
||||
@@ -349,11 +377,12 @@ class SharedAppViewModel(
|
||||
val onFailure = { action: Throwable ->
|
||||
intent {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(
|
||||
R.string.export_failed,
|
||||
": ${action.localizedMessage}",
|
||||
)
|
||||
),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -370,7 +399,10 @@ class SharedAppViewModel(
|
||||
if (it.exists()) it.delete()
|
||||
}
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.export_success))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.export_success),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
clearSelectedTunnels()
|
||||
}
|
||||
|
||||
+8
-2
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.InstalledPackageRepository
|
||||
@@ -88,9 +89,14 @@ class SplitTunnelViewModel(
|
||||
editableInterface.copy(includedApplications = included, excludedApplications = excluded)
|
||||
val updatedProxyConfig = editableConfig.copy(`interface` = updatedInterface)
|
||||
val updatedConfig = updatedProxyConfig.buildConfig()
|
||||
tunnelRepository.save(tunnel.copy(quickConfig = updatedConfig.asQuickString()))
|
||||
tunnelRepository.save(
|
||||
tunnel.copy(quickConfig = updatedConfig.withName(tunnel.name).asQuickString())
|
||||
)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.config_changes_saved))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.config_changes_saved),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||
}
|
||||
|
||||
+15
-6
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.AppUpdate
|
||||
@@ -25,7 +26,10 @@ class SupportViewModel(
|
||||
|
||||
fun checkForStandaloneUpdate() = intent {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(StringValue.StringResource(R.string.checking_for_update))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.checking_for_update),
|
||||
ToastType.Info,
|
||||
)
|
||||
)
|
||||
reduce { state.copy(isLoading = true) }
|
||||
updateRepository
|
||||
@@ -33,15 +37,19 @@ class SupportViewModel(
|
||||
.onSuccess { update ->
|
||||
if (update == null) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(
|
||||
StringValue.StringResource(R.string.latest_installed)
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.latest_installed),
|
||||
ToastType.Info,
|
||||
)
|
||||
)
|
||||
} else reduce { state.copy(appUpdate = update.sanitized()) }
|
||||
}
|
||||
.onFailure {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(StringValue.StringResource(R.string.update_check_failed))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.update_check_failed),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
reduce { state.copy(isLoading = false) }
|
||||
@@ -83,8 +91,9 @@ class SupportViewModel(
|
||||
.onSuccess { postSideEffect(GlobalSideEffect.InstallApk(it)) }
|
||||
.onFailure {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(
|
||||
StringValue.StringResource(R.string.update_download_failed)
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.update_download_failed),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<group
|
||||
android:scaleX="1.18"
|
||||
android:scaleY="1.18"
|
||||
android:pivotX="512"
|
||||
android:pivotY="512"
|
||||
android:translateX="-45"
|
||||
android:translateY="-45">
|
||||
<path
|
||||
android:pathData="M779.7,207.8C782.5,207.8 785.4,207.8 788.2,207.8C851.8,207.6 851.8,207.6 871.1,225.5C882.4,237.3 887.6,250.9 887.5,267.1C886.9,284.9 879,300.9 872,317C871,319.2 870.1,321.4 869.1,323.7C867.7,327.1 866.2,330.6 864.7,334.1C861,342.7 857.3,351.4 853.7,360.1C847.1,375.7 840.5,391.3 833.8,406.9C831.1,413.1 828.5,419.2 825.8,425.4C822.9,432.3 819.9,439.2 817,446C804.9,474 804.9,474 793.3,502.2C791,507.8 788.5,513.3 785.9,518.7C782,527 778.5,535.5 775,544C772,551 769,557.9 766.1,564.9C765.6,566 765.1,567.2 764.6,568.4C763.5,570.7 762.5,573.1 761.5,575.5C760,579 758.5,582.6 757,586.1C751.8,598.2 746.6,610.4 741.2,622.4C737.3,631.3 733.4,640.3 729.5,649.2C725.8,657.9 722,666.6 718,675.2C715.7,680.4 713.5,685.6 711.4,690.8C708.5,697.8 705.4,704.7 702.1,711.6C700.8,714.5 699.4,717.4 698,720.4C685.7,746.7 672.9,772.5 643.9,783.1C639.6,784.4 635.5,784.7 631,785C630.2,785.1 629.5,785.1 628.7,785.2C610.7,785.8 596.7,779.5 583.5,767.8C569.4,754.5 562,735.7 554.3,718.3C552.1,713.5 549.9,708.7 547.5,704.1C543.9,696.9 540.8,689.6 537.6,682.2C534.6,675 531.4,667.8 528.3,660.6C522.3,646.9 516.3,633.3 510.4,619.6C508.5,615.1 506.5,610.6 504.6,606.1C494.1,582.1 494.1,582.1 489.6,571.2C488,567.4 486.4,563.6 484.6,559.8C481.8,553.6 479.2,547.4 476.6,541.1C472.4,531.2 472.4,531.2 468,521.5C461.3,507.3 455.5,492.6 449.5,478.1C444.9,466.9 440.2,455.8 435.3,444.7C431.5,436.1 427.7,427.4 424.1,418.8C423.8,418.1 423.5,417.4 423.2,416.6C420,409.1 416.8,401.6 413.7,394C413.4,393.3 413.2,392.7 412.9,392C411.6,389 410.4,386 409.2,383C406.8,377.1 404.3,371.4 401.6,365.7C397.9,357.9 394.6,349.9 391.3,341.9C390.4,339.6 389.5,337.4 388.6,335.2C388,333.7 387.3,332.2 386.7,330.7C384.7,325.7 382.6,320.7 380.6,315.8C380,314.5 379.5,313.2 378.9,311.9C377.9,309.5 376.9,307.1 375.9,304.7C368.4,286.7 361.9,264.8 369.7,245.7C373.8,237.4 377.9,230 385,224C385.6,223.5 386.2,223 386.8,222.4C397,213.9 412.5,209.7 425.8,210.1C442.3,212.1 455.2,220.4 466,233C471.9,241.6 476.5,250.7 480.9,260.1C481.5,261.3 482.2,262.6 482.8,263.9C487.3,273.5 491.7,283.1 496,292.8C497,294.9 497.9,297.1 498.9,299.2C503.6,309.6 508.1,320.1 512.5,330.6C515.1,336.6 517.7,342.5 520.5,348.4C524.1,356.1 527.4,363.9 530.6,371.8C531.7,374.4 532.8,377 533.9,379.7C534.3,380.6 534.3,380.6 534.7,381.6C536.3,385.3 537.8,388.9 539.5,392.5C541.5,396.7 543.3,401 545.1,405.3C545.4,406 545.6,406.6 545.9,407.3C547,409.9 548.1,412.6 549.2,415.2C552.5,423.2 555.9,431.1 559.6,438.9C561.7,443.4 563.7,447.9 565.5,452.5C567.6,457.7 569.8,462.9 572.3,467.9C575.9,475.7 579.2,483.5 582.5,491.4C585.8,499.2 589.1,507 592.5,514.7C617.2,571.5 617.2,571.5 625.1,591.7C625.8,593.9 625.8,593.9 627,595C627.3,594.3 627.6,593.5 627.9,592.7C632.8,580 637.7,567.3 643.4,554.9C646.9,547.2 650.2,539.5 653.4,531.8C653.7,531.1 654,530.4 654.3,529.7C657.2,522.9 660,516 662.9,509.1C663.3,508 663.8,506.9 664.2,505.8C665,503.9 665.9,501.9 666.7,499.9C668.9,494.5 671.3,489.1 673.8,483.7C675.7,479.4 677.6,475 679.5,470.7C679.9,469.7 680.3,468.7 680.8,467.7C682.2,464.5 683.6,461.2 685,458C686,455.6 687.1,453.3 688.1,450.9C689.7,447.1 691.3,443.4 693,439.6C695.8,433.2 698.5,426.8 701.3,420.3C714.6,389.7 714.6,389.7 727.2,358.9C729.7,352.7 732.4,346.8 735.3,340.8C736.9,337.4 738.2,333.8 739.6,330.3C740.1,329.1 740.5,327.9 741,326.7C741.3,325.8 741.6,324.9 742,324C741.2,324 741.2,324 740.3,324C726.5,324.1 712.7,324.1 698.9,324.1C692.2,324.1 685.5,324.1 678.9,324.2C672.4,324.2 665.9,324.2 659.5,324.2C657,324.2 654.6,324.2 652.1,324.2C640.5,324.3 629,324.2 617.4,323.1C616.6,323.1 615.8,323 614.9,322.9C597,321 581.4,314.3 569,301C566.5,297.8 564.7,294.6 563,291C562.5,290 562,289 561.5,288C556.5,276.1 555.8,261.8 559.6,249.4C562,243.5 565.3,238.2 569,233C569.7,232 569.7,232 570.4,231C581.2,217.3 599.1,212.3 615.8,210.2C637.1,208 659,208.7 680.4,208.5C683.5,208.4 686.5,208.4 689.5,208.4C719.6,208.1 749.6,207.9 779.7,207.8Z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
<path
|
||||
android:pathData="M263.3,223.5C276.3,234.5 282.6,248.9 289.6,264C290.9,266.8 292.2,269.5 293.5,272.2C303,291.5 311.4,311.3 320,331C320.3,331.7 320.6,332.5 321,333.2C323.4,338.9 325.9,344.5 328.3,350.2C332.2,359.1 336.1,368.1 340,377C346.2,391.1 352.3,405.2 358.4,419.3C360.8,424.9 363.3,430.5 365.7,436.1C366.5,438 367.3,439.9 368.1,441.7C370.6,447.4 373,453 375.5,458.7C382.5,474.8 389.4,491 396.4,507.2C398.1,511.3 399.9,515.5 401.7,519.6C408.7,536 415.8,552.5 422.7,568.9C424,572 425.3,575.2 426.7,578.3C433,593.2 439.2,608.1 445.5,623C447.5,627.7 449.4,632.4 451.4,637C453.8,642.9 456.3,648.7 458.7,654.6C459.9,657.5 461.2,660.4 462.4,663.3C467.1,674.6 471.8,685.9 476.4,697.3C477.1,698.8 477.1,698.8 477.7,700.4C484.3,716.8 488.7,735.5 482,752.6C479.3,758.3 475.9,763.2 472,768C471.6,768.6 471.1,769.2 470.7,769.8C464.5,777.8 455.6,782.4 446,785C445,785.3 444,785.7 443,786C431.6,787 420.6,786.5 410,782C408.7,781.5 408.7,781.5 407.4,781C380.7,769.4 369.2,736.1 358.2,711.6C356.7,708.3 355.1,705 353.6,701.7C340.8,674.4 329,646.7 317.5,618.9C316,615.2 314.3,611.5 312.7,607.9C309.7,601.4 306.9,594.9 304.1,588.4C303.9,587.8 303.6,587.2 303.3,586.5C299.7,578 296.1,569.4 292.6,560.8C291,556.9 289.2,553 287.4,549.1C284.7,543 282,536.9 279.3,530.8C278.7,529.3 278.1,527.9 277.4,526.5C276.2,523.6 274.9,520.8 273.7,517.9C272.3,514.6 270.8,511.3 269.4,508C264.2,496.1 259.1,484.2 254.1,472.3C250.4,463.6 246.7,455 242.9,446.3C242.5,445.4 242.2,444.5 241.8,443.6C240,439.6 238.3,435.6 236.5,431.6C234.9,427.8 233.2,424.1 231.6,420.4C231.4,419.7 231.1,419.1 230.8,418.5C226.1,407.6 221.5,396.7 217,385.8C214.5,379.6 211.9,373.5 209.2,367.5C207.4,363.7 205.8,359.9 204.1,356.1C203.8,355.2 203.4,354.4 203,353.5C199.1,344.2 195.2,334.9 191.3,325.6C191,324.9 190.7,324.1 190.3,323.3C187.1,315.5 183.8,307.6 180.7,299.8C180.2,298.6 179.7,297.4 179.2,296.2C177.6,291.9 176.2,287.5 175,283C174.8,282.4 174.7,281.8 174.5,281.2C171.2,266.5 173.7,250.9 181.4,238.2C191.3,223.5 203.1,215.7 220.6,212.3C236.3,210.9 250.7,213.5 263.3,223.5Z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.8 KiB |
@@ -58,7 +58,7 @@
|
||||
<string name="wifi_settings">Wi-Fi settings</string>
|
||||
<string name="tunnel_on_wifi">Tunnel on Wi-Fi</string>
|
||||
<string name="add_peer">Add peer</string>
|
||||
<string name="backup_success">Backup success. %1$s</string>
|
||||
<string name="backup_success">Backup success</string>
|
||||
<string name="persistent_keepalive">Persistent keepalive</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="exclude">Exclude</string>
|
||||
|
||||
@@ -245,7 +245,7 @@
|
||||
<string name="root_required_template">%1$s (vyžaduje root)</string>
|
||||
<string name="recommended_template">%1$s (doporučeno)</string>
|
||||
<string name="hint_template">(%1$s)</string>
|
||||
<string name="backup_success">Úspěšně zazálohováno. %1$s</string>
|
||||
<string name="backup_success">Úspěšně zazálohováno</string>
|
||||
<string name="config_error_template">Špatná konfigurace. %1$s v umístění: %2$s.</string>
|
||||
<string name="donation_dev_message">Jako jediný vývojář neúnavně pracuji na tom, aby se WG Tunnel stal nejlepším bezplatným a open-source WireGuard klientem pro Android, ale to je možné pouze s vaší podporou.</string>
|
||||
<string name="google_donation_message">Bohužel, kvůli pravidlům společnosti Google nejsou odkazy na darování povoleny ve verzi této aplikace z Obchodu Play. Projděte si prosím webové stránky projektu, abyste zjistili, kde můžete přispět.</string>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
<string name="wifi_settings">Wi-Fi settings</string>
|
||||
<string name="tunnel_on_wifi">Tunnel on Wi-Fi</string>
|
||||
<string name="add_peer">Add peer</string>
|
||||
<string name="backup_success">Backup success. %1$s</string>
|
||||
<string name="backup_success">Backup success</string>
|
||||
<string name="persistent_keepalive">Persistent keepalive</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="exclude">Exclude</string>
|
||||
|
||||
@@ -138,6 +138,7 @@
|
||||
<string name="config_error">Ungültige Konfiguration</string>
|
||||
<string name="join_matrix">Matrix-Community beitreten</string>
|
||||
<string name="error_download_failed">Download der Konfiguration fehlgeschlagen</string>
|
||||
<string name="wg_url_confirm_message">Möchtest du wirklich Tunnel von %1$s hinzufügen? Verbinde dich niemals mit einem nicht vertrauenswürdigen VPN!</string>
|
||||
<string name="add_from_url">Von URL hinzufügen</string>
|
||||
<string name="export_logs">Gespeicherte Logs exportieren</string>
|
||||
<string name="app_permission_title">Steuere Tunnel und Auto-Tunnel Funktionen.</string>
|
||||
@@ -202,7 +203,7 @@
|
||||
<string name="auto_tunnel_running">Auto-Tunnel läuft</string>
|
||||
<string name="auto_tunnel_not_running">Auto-Tunnel läuft nicht</string>
|
||||
<string name="tunnel_monitoring">Tunnelüberwachung</string>
|
||||
<string name="backup_success">Backuperfolg. %1$s</string>
|
||||
<string name="backup_success">Backuperfolg</string>
|
||||
<string name="restore_success">Wiederherstellerfolg. %1$s</string>
|
||||
<string name="restarting_app">Starte App neu, um Änderungen anzuwenden …</string>
|
||||
<string name="restore_failed">Wiederherstellung aus Backup fehlgeschlagen.</string>
|
||||
|
||||
@@ -202,7 +202,7 @@
|
||||
<string name="auto_tunnel_running">El túnel automático está activo</string>
|
||||
<string name="auto_tunnel_not_running">El túnel automático no está activo</string>
|
||||
<string name="tunnel_monitoring">Monitoreo del túnel</string>
|
||||
<string name="backup_success">Copia de seguridad realizada con éxito. %1$s</string>
|
||||
<string name="backup_success">Copia de seguridad realizada con éxito</string>
|
||||
<string name="restore_success">Restauración completada. %1$s</string>
|
||||
<string name="restarting_app">Reiniciando la app para aplicar los cambios…</string>
|
||||
<string name="restore_failed">No se pudo restaurar la copia de seguridad.</string>
|
||||
|
||||
@@ -194,7 +194,7 @@
|
||||
<string name="recommended_template">%1$s (soovitatav)</string>
|
||||
<string name="hint_template">(%1$s)</string>
|
||||
<string name="tunnel_monitoring">Tunnelo monitooring</string>
|
||||
<string name="backup_success">Varundus õnnestus. %1$s</string>
|
||||
<string name="backup_success">Varundus õnnestus</string>
|
||||
<string name="restore_success">Taastamine õnnestus. %1$s</string>
|
||||
<string name="restarting_app">Muudatuste jõustamiseks taaskäivitan rakenduse…</string>
|
||||
<string name="restore_failed">Varukoopiast taastamine ei õnnestunud.</string>
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
<string name="wifi_settings">Wi-Fi settings</string>
|
||||
<string name="tunnel_on_wifi">Tunnel on Wi-Fi</string>
|
||||
<string name="add_peer">Add peer</string>
|
||||
<string name="backup_success">Backup success. %1$s</string>
|
||||
<string name="backup_success">Backup success</string>
|
||||
<string name="persistent_keepalive">Persistent keepalive</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="exclude">Exclude</string>
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
<string name="show_qr">Show QR</string>
|
||||
<string name="wifi_settings">Wi-Fi settings</string>
|
||||
<string name="add_peer">Add peer</string>
|
||||
<string name="backup_success">Backup success. %1$s</string>
|
||||
<string name="backup_success">Backup success</string>
|
||||
<string name="persistent_keepalive">Persistent keepalive</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="backup_failed">Failed to create backup.</string>
|
||||
|
||||
@@ -171,7 +171,7 @@
|
||||
<string name="mimic_quic">Imiter QUIC</string>
|
||||
<string name="show_qr">Afficher le QR</string>
|
||||
<string name="wifi_settings">Paramètres Wi-Fi</string>
|
||||
<string name="backup_success">Sauvegarde réussie. %1$s</string>
|
||||
<string name="backup_success">Sauvegarde réussie</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="backup_failed">Échec de la création de la sauvegarde.</string>
|
||||
<string name="location_permissions">Permissions de localisation</string>
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
<string name="wifi_settings">Wi-Fi settings</string>
|
||||
<string name="tunnel_on_wifi">Tunnel on Wi-Fi</string>
|
||||
<string name="add_peer">Add peer</string>
|
||||
<string name="backup_success">Backup success. %1$s</string>
|
||||
<string name="backup_success">Backup success</string>
|
||||
<string name="persistent_keepalive">Persistent keepalive</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="exclude">Exclude</string>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
<string name="wifi_settings">Wi-Fi beállítások</string>
|
||||
<string name="tunnel_on_wifi">Alagút Wi-Fi-n</string>
|
||||
<string name="add_peer">Peer hozzáadása</string>
|
||||
<string name="backup_success">Sikeres mentés. %1$s</string>
|
||||
<string name="backup_success">Sikeres mentés</string>
|
||||
<string name="persistent_keepalive">Kapcsolatmegőrzés</string>
|
||||
<string name="info">Infó</string>
|
||||
<string name="exclude">Kizárás</string>
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
<string name="auto_tunnel_channel_description">Saluran untuk notifikasi status terowongan otomatis</string>
|
||||
<string name="show_qr">Tampilkan QR</string>
|
||||
<string name="wifi_settings">Pengaturan Wi-Fi</string>
|
||||
<string name="backup_success">Pencadangan berhasil. %1$s</string>
|
||||
<string name="backup_success">Pencadangan berhasil</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="exclude">Kecualikan</string>
|
||||
<string name="backup_failed">Gagal membuat cadangan.</string>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user