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:
|
outputs:
|
||||||
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
|
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@v5
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '17'
|
java-version: '21'
|
||||||
cache: gradle
|
cache: gradle
|
||||||
|
|
||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
|
|||||||
@@ -72,15 +72,15 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
|
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@v5
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '17'
|
java-version: '21'
|
||||||
cache: gradle
|
cache: gradle
|
||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
@@ -112,6 +112,9 @@ jobs:
|
|||||||
./gradlew :app:assemble${flavor^}Debug --stacktrace
|
./gradlew :app:assemble${flavor^}Debug --stacktrace
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
env:
|
||||||
|
GITHUB_SHA: ${{ github.sha }}
|
||||||
|
GITHUB_RUN_NUMBER: ${{ github.run_number }}
|
||||||
- name: Get release apk path
|
- name: Get release apk path
|
||||||
id: apk-path
|
id: apk-path
|
||||||
run: echo "path=$(find . -regex '^.*/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT
|
run: echo "path=$(find . -regex '^.*/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
has_new_commits: ${{ steps.check.outputs.new_commits }}
|
has_new_commits: ${{ steps.check.outputs.new_commits }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
- name: Check for new commits
|
- name: Check for new commits
|
||||||
id: check
|
id: check
|
||||||
env:
|
env:
|
||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,26 @@
|
|||||||
name: on-pr
|
name: on-pr
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
format_check:
|
format_check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
- name: Set up JDK 17
|
|
||||||
|
- name: Verify Gradle Wrapper
|
||||||
|
uses: gradle/actions/wrapper-validation@v6
|
||||||
|
|
||||||
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@v5
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '17'
|
java-version: '21'
|
||||||
cache: gradle
|
cache: gradle
|
||||||
|
|
||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ jobs:
|
|||||||
name: publish-github
|
name: publish-github
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event_name == 'push' && github.ref || 'master' }}
|
ref: ${{ github.event_name == 'push' && github.ref || 'master' }}
|
||||||
- name: Install system dependencies
|
- name: Install system dependencies
|
||||||
@@ -205,7 +205,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: build-google-aab
|
needs: build-google-aab
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event_name == 'push' && github.ref || 'master' }}
|
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.dsl.ApplicationExtension
|
||||||
import com.android.build.api.variant.FilterConfiguration
|
import com.android.build.api.variant.FilterConfiguration
|
||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
@@ -23,13 +22,6 @@ licensee {
|
|||||||
ignoreDependencies("com.github.topjohnwu.libsu")
|
ignoreDependencies("com.github.topjohnwu.libsu")
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
|
||||||
compilerOptions {
|
|
||||||
jvmTarget = JvmTarget.JVM_17
|
|
||||||
freeCompilerArgs = listOf("-XXLanguage:+PropertyParamAnnotationDefaultTargetMode")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
configure<ApplicationExtension> {
|
configure<ApplicationExtension> {
|
||||||
namespace = Constants.APP_ID
|
namespace = Constants.APP_ID
|
||||||
compileSdk = Constants.TARGET_SDK
|
compileSdk = Constants.TARGET_SDK
|
||||||
@@ -46,10 +38,11 @@ configure<ApplicationExtension> {
|
|||||||
|
|
||||||
splits {
|
splits {
|
||||||
abi {
|
abi {
|
||||||
isEnable = !project.hasProperty("noSplits")
|
val noSplits = providers.gradleProperty("noSplits").isPresent
|
||||||
|
isEnable = !noSplits
|
||||||
reset()
|
reset()
|
||||||
include("armeabi-v7a", "arm64-v8a")
|
include("armeabi-v7a", "arm64-v8a")
|
||||||
isUniversalApk = !project.hasProperty("noSplits")
|
isUniversalApk = !noSplits
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,14 +50,17 @@ configure<ApplicationExtension> {
|
|||||||
applicationId = Constants.APP_ID
|
applicationId = Constants.APP_ID
|
||||||
minSdk = Constants.MIN_SDK
|
minSdk = Constants.MIN_SDK
|
||||||
targetSdk = Constants.TARGET_SDK
|
targetSdk = Constants.TARGET_SDK
|
||||||
versionCode = computeVersionCode()
|
versionCode = Constants.VERSION_CODE
|
||||||
versionName = computeVersionName()
|
versionName = Constants.VERSION_NAME
|
||||||
|
|
||||||
|
experimentalProperties["android.experimental.disableGitVersion"] = true
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
getByName("debug").assets.directories += "$projectDir/schemas"
|
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 }")
|
buildConfigField("String[]", "LANGUAGES", "new String[]{ $languagesArray }")
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
@@ -134,8 +130,6 @@ configure<ApplicationExtension> {
|
|||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
isCoreLibraryDesugaringEnabled = true
|
isCoreLibraryDesugaringEnabled = true
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
@@ -148,31 +142,42 @@ configure<ApplicationExtension> {
|
|||||||
|
|
||||||
androidComponents {
|
androidComponents {
|
||||||
onVariants { variant ->
|
onVariants { variant ->
|
||||||
|
val isNightly = project.isNightlyBuild()
|
||||||
|
|
||||||
val abiNameMap =
|
if (isNightly) {
|
||||||
mapOf(
|
variant.outputs.forEach { output ->
|
||||||
"armeabi-v7a" to "armv7",
|
|
||||||
"arm64-v8a" to "arm64",
|
output.versionCode.set(
|
||||||
"x86" to "x86",
|
output.versionCode.get() + project.getVersionCodeIncrement()
|
||||||
"x86_64" to "x64",
|
)
|
||||||
)
|
|
||||||
|
val currentVersion = output.versionName.get()
|
||||||
|
val nextVersion = bumpToNextPatchVersion(currentVersion)
|
||||||
|
val gitHash = project.getGitCommitHash()
|
||||||
|
|
||||||
|
output.versionName.set("$nextVersion-nightly+git.$gitHash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val abiNameMap = mapOf(
|
||||||
|
"armeabi-v7a" to "armv7",
|
||||||
|
"arm64-v8a" to "arm64",
|
||||||
|
"x86" to "x86",
|
||||||
|
"x86_64" to "x64",
|
||||||
|
)
|
||||||
|
|
||||||
variant.outputs.forEach { output ->
|
variant.outputs.forEach { output ->
|
||||||
val abi = output.filters.find { it.filterType == FilterConfiguration.FilterType.ABI }?.identifier
|
val abi = output.filters.find { it.filterType == FilterConfiguration.FilterType.ABI }?.identifier
|
||||||
|
|
||||||
val flavorName = variant.productFlavors.joinToString("-") { it.second }
|
val flavorName = variant.productFlavors.joinToString("-") { it.second }
|
||||||
|
|
||||||
val versionName = output.versionName.get()
|
val versionName = output.versionName.get()
|
||||||
|
|
||||||
val baseFileName = "${Constants.APP_NAME}-${flavorName}-v${versionName}"
|
val baseFileName = "${Constants.APP_NAME}-${flavorName}-v${versionName}"
|
||||||
|
|
||||||
val outputFileName =
|
val outputFileName = if (!abi.isNullOrEmpty()) {
|
||||||
if (!abi.isNullOrEmpty()) {
|
val shortAbiName = abiNameMap.getOrDefault(abi, abi)
|
||||||
val shortAbiName = abiNameMap.getOrDefault(abi, abi)
|
"${baseFileName}-${shortAbiName}.apk"
|
||||||
"${baseFileName}-${shortAbiName}.apk"
|
} else {
|
||||||
} else {
|
"${baseFileName}.apk"
|
||||||
"${baseFileName}.apk"
|
}
|
||||||
}
|
|
||||||
|
|
||||||
output.outputFileName.set(outputFileName)
|
output.outputFileName.set(outputFileName)
|
||||||
}
|
}
|
||||||
@@ -225,6 +230,7 @@ dependencies {
|
|||||||
// UI utilities
|
// UI utilities
|
||||||
implementation(libs.bundles.ui.utilities)
|
implementation(libs.bundles.ui.utilities)
|
||||||
implementation(libs.lottie.compose)
|
implementation(libs.lottie.compose)
|
||||||
|
implementation(libs.sonner)
|
||||||
|
|
||||||
// Misc utilities
|
// Misc utilities
|
||||||
implementation(libs.bundles.misc.utilities)
|
implementation(libs.bundles.misc.utilities)
|
||||||
|
|||||||
@@ -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:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.App.Start"
|
android:theme="@style/Theme.App.Start"
|
||||||
tools:targetApi="tiramisu">
|
tools:targetApi="tiramisu">
|
||||||
@@ -74,6 +73,13 @@
|
|||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="wg" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<action android:name="android.intent.action.SHOW_APP_INFO" />
|
<action android:name="android.intent.action.SHOW_APP_INFO" />
|
||||||
@@ -169,9 +175,9 @@
|
|||||||
tools:node="remove" />
|
tools:node="remove" />
|
||||||
</provider>
|
</provider>
|
||||||
<service
|
<service
|
||||||
android:name=".core.service.tile.TunnelControlTile"
|
android:name=".service.tile.TunnelControlTile"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:icon="@drawable/ic_notification"
|
android:icon="@drawable/ic_qs_logo"
|
||||||
android:label="@string/tunnel_control"
|
android:label="@string/tunnel_control"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||||
<meta-data
|
<meta-data
|
||||||
@@ -186,9 +192,9 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name=".core.service.tile.AutoTunnelControlTile"
|
android:name=".service.tile.AutoTunnelControlTile"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:icon="@drawable/ic_notification"
|
android:icon="@drawable/ic_qs_logo"
|
||||||
android:label="@string/auto_tunnel"
|
android:label="@string/auto_tunnel"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||||
<meta-data
|
<meta-data
|
||||||
@@ -203,7 +209,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name=".core.service.autotunnel.AutoTunnelService"
|
android:name=".service.autotunnel.AutoTunnelService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="specialUse"
|
android:foregroundServiceType="specialUse"
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel
|
package com.zaneschepke.wireguardautotunnel
|
||||||
|
|
||||||
import ProxySettingsScreen
|
import ProxySettingsScreen
|
||||||
|
import android.Manifest
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.provider.Settings
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.activity.SystemBarStyle
|
import androidx.activity.SystemBarStyle
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
@@ -19,17 +21,29 @@ import androidx.compose.animation.fadeOut
|
|||||||
import androidx.compose.animation.slideInHorizontally
|
import androidx.compose.animation.slideInHorizontally
|
||||||
import androidx.compose.animation.slideOutHorizontally
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.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.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -38,20 +52,19 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.LinkAnnotation
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.SpanStyle
|
|
||||||
import androidx.compose.ui.text.TextLinkStyles
|
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
|
||||||
import androidx.compose.ui.text.intl.Locale
|
import androidx.compose.ui.text.intl.Locale
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.text.withLink
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||||
@@ -59,6 +72,10 @@ import androidx.navigation3.runtime.entryProvider
|
|||||||
import androidx.navigation3.runtime.rememberNavBackStack
|
import androidx.navigation3.runtime.rememberNavBackStack
|
||||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||||
import androidx.navigation3.ui.NavDisplay
|
import androidx.navigation3.ui.NavDisplay
|
||||||
|
import com.dokar.sonner.TextToastAction
|
||||||
|
import com.dokar.sonner.ToastType
|
||||||
|
import com.dokar.sonner.Toaster
|
||||||
|
import com.dokar.sonner.rememberToasterState
|
||||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||||
@@ -69,11 +86,9 @@ import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
|
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.banner.AppAlertBanner
|
import com.zaneschepke.wireguardautotunnel.ui.common.banner.AppAlertBanner
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.LocalNetworkPermissionDialog
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
|
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarInfo
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarType
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.rememberCustomSnackbarState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.SecureRoute
|
import com.zaneschepke.wireguardautotunnel.ui.navigation.SecureRoute
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Tab
|
import com.zaneschepke.wireguardautotunnel.ui.navigation.Tab
|
||||||
@@ -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.sort.SortScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.SplitTunnelScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.SplitTunnelScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.AlertRed
|
import com.zaneschepke.wireguardautotunnel.ui.theme.AlertRed
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.Heart
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.OffWhite
|
import com.zaneschepke.wireguardautotunnel.ui.theme.OffWhite
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.Straw
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.installApk
|
import com.zaneschepke.wireguardautotunnel.util.extensions.installApk
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.restartApp
|
import com.zaneschepke.wireguardautotunnel.util.extensions.restartApp
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
import com.zaneschepke.wireguardautotunnel.util.permission.LocalNetworkPermissionHelper
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigEditViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigEditViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
|
||||||
|
import de.raphaelebner.roomdatabasebackup.core.OnCompleteListener.Companion.EXIT_CODE_ERROR
|
||||||
|
import de.raphaelebner.roomdatabasebackup.core.OnCompleteListener.Companion.EXIT_CODE_ERROR_DECRYPTION_ERROR
|
||||||
|
import de.raphaelebner.roomdatabasebackup.core.OnCompleteListener.Companion.EXIT_CODE_ERROR_RESTORE_BACKUP_IS_ENCRYPTED
|
||||||
|
import de.raphaelebner.roomdatabasebackup.core.OnCompleteListener.Companion.EXIT_CODE_ERROR_WRONG_DECRYPTION_PASSWORD
|
||||||
import de.raphaelebner.roomdatabasebackup.core.RoomBackup
|
import de.raphaelebner.roomdatabasebackup.core.RoomBackup
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlinx.coroutines.awaitCancellation
|
import kotlinx.coroutines.awaitCancellation
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
@@ -132,6 +157,7 @@ import org.koin.androidx.compose.koinViewModel
|
|||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
import org.orbitmvi.orbit.compose.collectAsState
|
import org.orbitmvi.orbit.compose.collectAsState
|
||||||
|
import timber.log.Timber
|
||||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
@@ -155,9 +181,10 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
handleIncomingIntent(intent)
|
roomBackup = RoomBackup(this).database(appDatabase).enableLogDebug(true).maxFileCount(5)
|
||||||
|
|
||||||
roomBackup = RoomBackup(this)
|
handleConfigFileIntent(intent)
|
||||||
|
handleWgDeepLinkIntent(intent)
|
||||||
|
|
||||||
installSplashScreen().apply {
|
installSplashScreen().apply {
|
||||||
setKeepOnScreenCondition { !viewModel.container.stateFlow.value.isAppLoaded }
|
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 showVpnPermissionDialog by remember { mutableStateOf(false) }
|
||||||
var vpnPermissionDenied by remember { mutableStateOf(false) }
|
var vpnPermissionDenied by remember { mutableStateOf(false) }
|
||||||
var requestingTunnelMode by remember {
|
var requestingTunnelMode by remember {
|
||||||
mutableStateOf<Pair<TunnelMode?, TunnelConfig?>>(Pair(null, null))
|
mutableStateOf<Pair<TunnelMode?, TunnelConfig?>>(Pair(null, null))
|
||||||
}
|
}
|
||||||
|
var showLocalNetworkRationale by remember { mutableStateOf(false) }
|
||||||
|
var hasPromptedLocalNetwork by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val localNetworkPermissionLauncher =
|
||||||
|
rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.RequestPermission()
|
||||||
|
) { isGranted ->
|
||||||
|
if (!isGranted) {
|
||||||
|
val canAskAgain =
|
||||||
|
ActivityCompat.shouldShowRequestPermissionRationale(
|
||||||
|
this,
|
||||||
|
Manifest.permission.ACCESS_LOCAL_NETWORK,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!canAskAgain) {
|
||||||
|
val intent =
|
||||||
|
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||||
|
data = Uri.fromParts("package", packageName, null)
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
} else {
|
||||||
|
toaster.show(
|
||||||
|
message =
|
||||||
|
context.getString(R.string.local_network_permission_denied),
|
||||||
|
type = ToastType.Warning,
|
||||||
|
duration = 6000.milliseconds,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(uiState.isAppLoaded) {
|
||||||
|
if (
|
||||||
|
uiState.isAppLoaded &&
|
||||||
|
!hasPromptedLocalNetwork &&
|
||||||
|
LocalNetworkPermissionHelper.shouldRequestPermission() &&
|
||||||
|
!LocalNetworkPermissionHelper.isPermissionGranted(context)
|
||||||
|
) {
|
||||||
|
hasPromptedLocalNetwork = true
|
||||||
|
showLocalNetworkRationale = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val startingStack = buildList {
|
val startingStack = buildList {
|
||||||
add(Route.Tunnels)
|
add(Route.Tunnels)
|
||||||
@@ -232,22 +301,18 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
is GlobalSideEffect.Snackbar -> {
|
is GlobalSideEffect.Snackbar -> {
|
||||||
scope.launch {
|
when (sideEffect.type) {
|
||||||
snackbarState.showSnackbar(
|
ToastType.Warning,
|
||||||
SnackbarInfo(
|
ToastType.Error -> toaster.dismissAll()
|
||||||
message =
|
else -> Unit
|
||||||
buildAnnotatedString {
|
|
||||||
append(sideEffect.message.asString(context))
|
|
||||||
},
|
|
||||||
type = sideEffect.type ?: SnackbarType.INFO,
|
|
||||||
durationMs = sideEffect.durationMs ?: 4000L,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is GlobalSideEffect.Toast ->
|
toaster.show(
|
||||||
scope.launch { context.showToast(sideEffect.message.asString(context)) }
|
message = sideEffect.message.asString(context),
|
||||||
|
type = sideEffect.type,
|
||||||
|
duration = (sideEffect.durationMs ?: 4000L).milliseconds,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
is GlobalSideEffect.LaunchUrl -> context.openWebUrl(sideEffect.url)
|
is GlobalSideEffect.LaunchUrl -> context.openWebUrl(sideEffect.url)
|
||||||
is GlobalSideEffect.InstallApk -> context.installApk(sideEffect.apk)
|
is GlobalSideEffect.InstallApk -> context.installApk(sideEffect.apk)
|
||||||
@@ -275,49 +340,58 @@ class MainActivity : AppCompatActivity() {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
val annotatedMessage = buildAnnotatedString {
|
if (showLocalNetworkRationale) {
|
||||||
append(context.getString(R.string.donation_prompt_prefix))
|
LocalNetworkPermissionDialog(
|
||||||
append(" ")
|
onDismiss = {
|
||||||
withLink(
|
showLocalNetworkRationale = false
|
||||||
LinkAnnotation.Clickable(
|
toaster.show(
|
||||||
tag = context.getString(R.string.support),
|
message =
|
||||||
styles =
|
context.getString(R.string.local_network_permission_denied),
|
||||||
TextLinkStyles(
|
type = ToastType.Warning,
|
||||||
style =
|
duration = 6000.milliseconds,
|
||||||
SpanStyle(
|
)
|
||||||
textDecoration = TextDecoration.Underline,
|
},
|
||||||
color = MaterialTheme.colorScheme.primary,
|
onAttest = {
|
||||||
),
|
showLocalNetworkRationale = false
|
||||||
focusedStyle =
|
|
||||||
SpanStyle(
|
localNetworkPermissionLauncher.launch(
|
||||||
textDecoration = TextDecoration.Underline,
|
Manifest.permission.ACCESS_LOCAL_NETWORK
|
||||||
color = MaterialTheme.colorScheme.primary,
|
)
|
||||||
background =
|
},
|
||||||
MaterialTheme.colorScheme.primary.copy(
|
)
|
||||||
alpha = 0.2f
|
}
|
||||||
),
|
|
||||||
),
|
uiState.pendingWgImportUrl?.let { url ->
|
||||||
),
|
val host = Uri.parse(url).host ?: url
|
||||||
) {
|
InfoDialog(
|
||||||
snackbarState.dismissCurrent()
|
onDismiss = { viewModel.dismissWgImport() },
|
||||||
navController.push(Route.Donate)
|
onAttest = { viewModel.importFromUrl(url) },
|
||||||
}
|
title = stringResource(R.string.add_from_url),
|
||||||
) {
|
body = { Text(stringResource(R.string.wg_url_confirm_message, host)) },
|
||||||
append(context.getString(R.string.donation_prompt_link))
|
confirmText = stringResource(R.string.okay),
|
||||||
}
|
)
|
||||||
append(" ")
|
|
||||||
append(context.getString(R.string.donation_prompt_suffix))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (uiState.shouldShowDonationSnackbar && !uiState.alreadyDonated) {
|
if (uiState.shouldShowDonationSnackbar && !uiState.alreadyDonated) {
|
||||||
viewModel.setShouldShowDonationSnackbar(false)
|
viewModel.setShouldShowDonationSnackbar(false)
|
||||||
snackbarState.showSnackbar(
|
toaster.show(
|
||||||
SnackbarInfo(
|
message =
|
||||||
message = annotatedMessage,
|
context.getString(R.string.donation_prompt_prefix) +
|
||||||
type = SnackbarType.THANK_YOU,
|
" " +
|
||||||
durationMs = 30_000L,
|
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(
|
Scaffold(
|
||||||
snackbarHost = {
|
|
||||||
snackbarState.SnackbarHost(
|
|
||||||
modifier =
|
|
||||||
Modifier.align(Alignment.BottomCenter)
|
|
||||||
.padding(bottom = 80.dp)
|
|
||||||
) { info ->
|
|
||||||
CustomSnackBar(
|
|
||||||
message = info.message,
|
|
||||||
type = info.type,
|
|
||||||
onDismiss = { snackbarState.dismissCurrent() },
|
|
||||||
containerColor =
|
|
||||||
MaterialTheme.colorScheme.surfaceColorAtElevation(
|
|
||||||
2.dp
|
|
||||||
),
|
|
||||||
modifier =
|
|
||||||
Modifier.wrapContentHeight(align = Alignment.Top),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
topBar = { DynamicTopAppBar(navState) },
|
topBar = { DynamicTopAppBar(navState) },
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
if (navState.showBottomItems) {
|
if (navState.showBottomItems) {
|
||||||
@@ -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 {
|
private fun handleWgDeepLinkIntent(intent: Intent) {
|
||||||
roomBackup
|
if (intent.action == Intent.ACTION_VIEW) {
|
||||||
.database(appDatabase)
|
val uri = intent.data ?: return
|
||||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
if (uri.scheme == "wg") {
|
||||||
.enableLogDebug(true)
|
val httpsUrl = uri.toString().replaceFirst("wg://", "https://")
|
||||||
.maxFileCount(5)
|
viewModel.promptWgImport(httpsUrl)
|
||||||
.apply {
|
|
||||||
onCompleteListener { success, _, _ ->
|
|
||||||
lifecycleScope.launch {
|
|
||||||
if (success) {
|
|
||||||
showToast(
|
|
||||||
getString(
|
|
||||||
R.string.backup_success,
|
|
||||||
getString(R.string.restarting_app),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
restartApp()
|
|
||||||
} else {
|
|
||||||
showToast(R.string.backup_failed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.backup()
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun performRestore() = lifecycleScope.launch {
|
|
||||||
roomBackup
|
|
||||||
.database(appDatabase)
|
|
||||||
.enableLogDebug(true)
|
|
||||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
|
||||||
.apply {
|
|
||||||
onCompleteListener { success, _, _ ->
|
|
||||||
lifecycleScope.launch {
|
|
||||||
if (success) {
|
|
||||||
showToast(
|
|
||||||
getString(
|
|
||||||
R.string.restore_success,
|
|
||||||
getString(R.string.restarting_app),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
restartApp()
|
|
||||||
} else {
|
|
||||||
showToast(R.string.restore_failed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.restore()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -611,21 +689,105 @@ class MainActivity : AppCompatActivity() {
|
|||||||
networkMonitor.checkPermissionsAndUpdateState()
|
networkMonitor.checkPermissionsAndUpdateState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun performBackup(encrypt: Boolean = false, password: String? = null) {
|
||||||
|
roomBackup
|
||||||
|
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||||
|
.apply {
|
||||||
|
if (encrypt && !password.isNullOrBlank()) {
|
||||||
|
backupIsEncrypted(true)
|
||||||
|
customEncryptPassword(password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onCompleteListener { success, _, _ ->
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val sideEffect =
|
||||||
|
if (success) {
|
||||||
|
GlobalSideEffect.Snackbar(
|
||||||
|
StringValue.StringResource(R.string.backup_success),
|
||||||
|
ToastType.Success,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
GlobalSideEffect.Snackbar(
|
||||||
|
StringValue.StringResource(R.string.backup_failed),
|
||||||
|
ToastType.Error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
viewModel.postSideEffect(sideEffect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.backup()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun performRestore(encrypt: Boolean = false, password: String? = null) {
|
||||||
|
roomBackup
|
||||||
|
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||||
|
.apply {
|
||||||
|
if (encrypt && !password.isNullOrBlank()) {
|
||||||
|
backupIsEncrypted(true)
|
||||||
|
customEncryptPassword(password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onCompleteListener { success, message, exitCode ->
|
||||||
|
lifecycleScope.launch {
|
||||||
|
if (success) {
|
||||||
|
viewModel.postSideEffect(
|
||||||
|
GlobalSideEffect.Snackbar(
|
||||||
|
StringValue.StringResource(R.string.restore_success),
|
||||||
|
ToastType.Success,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
roomBackup.restartApp(Intent(this@MainActivity, MainActivity::class.java))
|
||||||
|
} else {
|
||||||
|
Timber.w("Restore failed, exitCode=$exitCode, message=$message")
|
||||||
|
|
||||||
|
val errorMessage =
|
||||||
|
when (exitCode) {
|
||||||
|
EXIT_CODE_ERROR_WRONG_DECRYPTION_PASSWORD ->
|
||||||
|
getString(R.string.restore_failed_wrong_password)
|
||||||
|
|
||||||
|
EXIT_CODE_ERROR,
|
||||||
|
EXIT_CODE_ERROR_DECRYPTION_ERROR,
|
||||||
|
EXIT_CODE_ERROR_RESTORE_BACKUP_IS_ENCRYPTED ->
|
||||||
|
getString(R.string.restore_failed_invalid_file)
|
||||||
|
|
||||||
|
else -> getString(R.string.restore_failed)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.postSideEffect(
|
||||||
|
GlobalSideEffect.Snackbar(
|
||||||
|
StringValue.DynamicString(errorMessage),
|
||||||
|
ToastType.Error,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.restore()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
setIntent(intent)
|
setIntent(intent)
|
||||||
handleIncomingIntent(intent)
|
handleConfigFileIntent(intent)
|
||||||
|
handleWgDeepLinkIntent(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleIncomingIntent(intent: Intent?) {
|
private fun handleConfigFileIntent(intent: Intent?) {
|
||||||
intent ?: return
|
intent ?: return
|
||||||
|
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
Intent.ACTION_VIEW,
|
Intent.ACTION_VIEW,
|
||||||
Intent.ACTION_EDIT,
|
Intent.ACTION_EDIT,
|
||||||
Intent.ACTION_SEND -> {
|
Intent.ACTION_SEND -> {
|
||||||
val uri: Uri? = intent.data
|
val uri: Uri? = intent.data ?: return
|
||||||
uri?.let { viewModel.importFromUri(it) }
|
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.di.tunnelModule
|
||||||
import com.zaneschepke.tunnel.service.VpnService
|
import com.zaneschepke.tunnel.service.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.core.event.TunnelEventDispatcher
|
import com.zaneschepke.wireguardautotunnel.core.event.TunnelEventDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AppBoostrapCoordinator
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.AppBoostrapCoordinator
|
||||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelTileRefresher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelTileRefresher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||||
import com.zaneschepke.wireguardautotunnel.di.Dispatcher
|
import com.zaneschepke.wireguardautotunnel.di.Dispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.di.Scope
|
import com.zaneschepke.wireguardautotunnel.di.Scope
|
||||||
@@ -21,6 +18,9 @@ import com.zaneschepke.wireguardautotunnel.di.dispatchersModule
|
|||||||
import com.zaneschepke.wireguardautotunnel.di.networkModule
|
import com.zaneschepke.wireguardautotunnel.di.networkModule
|
||||||
import com.zaneschepke.wireguardautotunnel.di.tunnelBackendProviderModule
|
import com.zaneschepke.wireguardautotunnel.di.tunnelBackendProviderModule
|
||||||
import com.zaneschepke.wireguardautotunnel.di.workerModule
|
import com.zaneschepke.wireguardautotunnel.di.workerModule
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelTileRefresher
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelTileRefresher
|
||||||
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -51,6 +51,13 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
|||||||
|
|
||||||
private val backend: Backend by inject()
|
private val backend: Backend by inject()
|
||||||
|
|
||||||
|
private val alwaysOnCallback =
|
||||||
|
object : VpnService.AlwaysOnCallback {
|
||||||
|
override fun alwaysOnTriggered() {
|
||||||
|
applicationScope.launch { tunnelCoordinator.startDefault() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(KoinViewModelScopeApi::class)
|
@OptIn(KoinViewModelScopeApi::class)
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
@@ -71,11 +78,10 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
|||||||
lazyModules(networkModule)
|
lazyModules(networkModule)
|
||||||
}
|
}
|
||||||
instance = this
|
instance = this
|
||||||
|
|
||||||
notificationService.createAllChannels()
|
notificationService.createAllChannels()
|
||||||
|
|
||||||
// Sync tiles
|
syncTiles()
|
||||||
AutoTunnelTileRefresher.refresh(this)
|
|
||||||
TunnelTileRefresher.refresh(this)
|
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
Timber.plant(Timber.DebugTree())
|
Timber.plant(Timber.DebugTree())
|
||||||
@@ -87,13 +93,7 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
|||||||
Timber.plant(ReleaseTree())
|
Timber.plant(ReleaseTree())
|
||||||
}
|
}
|
||||||
|
|
||||||
backend.setAlwaysOnCallback(
|
backend.setAlwaysOnCallback(alwaysOnCallback)
|
||||||
object : VpnService.AlwaysOnCallback {
|
|
||||||
override fun alwaysOnTriggered() {
|
|
||||||
applicationScope.launch { tunnelCoordinator.startDefault() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val dispatcher = get<TunnelEventDispatcher>()
|
val dispatcher = get<TunnelEventDispatcher>()
|
||||||
val coordinator = get<TunnelCoordinator>()
|
val coordinator = get<TunnelCoordinator>()
|
||||||
@@ -111,6 +111,11 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
|||||||
applicationScope.launch(ioDispatcher) { boostrapCoordinator.bootstrap() }
|
applicationScope.launch(ioDispatcher) { boostrapCoordinator.bootstrap() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun syncTiles() {
|
||||||
|
AutoTunnelTileRefresher.refresh(this)
|
||||||
|
TunnelTileRefresher.refresh(this)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
lateinit var instance: WireGuardAutoTunnel
|
lateinit var instance: WireGuardAutoTunnel
|
||||||
private set
|
private set
|
||||||
|
|||||||
+1
-1
@@ -3,11 +3,11 @@ package com.zaneschepke.wireguardautotunnel.core.broadcast
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
||||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||||
import com.zaneschepke.wireguardautotunnel.di.Scope
|
import com.zaneschepke.wireguardautotunnel.di.Scope
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
|
|||||||
+156
-18
@@ -1,14 +1,19 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.event
|
package com.zaneschepke.wireguardautotunnel.core.event
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.dokar.sonner.ToastType
|
||||||
import com.zaneschepke.tunnel.event.TunnelEvent
|
import com.zaneschepke.tunnel.event.TunnelEvent
|
||||||
import com.zaneschepke.tunnel.model.BackendMode
|
import com.zaneschepke.tunnel.model.BackendMode
|
||||||
import com.zaneschepke.tunnel.state.BackendStatus
|
import com.zaneschepke.tunnel.state.BackendStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.TunnelNotificationLine
|
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.TunnelNotificationService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||||
|
import com.zaneschepke.wireguardautotunnel.lifecyle.AppVisibilityObserver
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationLine
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationService
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
@@ -20,11 +25,14 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
|||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class TunnelEventDispatcher(
|
class TunnelEventDispatcher(
|
||||||
private val notificationManager: TunnelNotificationService,
|
private val notificationManager: TunnelNotificationService,
|
||||||
private val tunnelRepository: TunnelRepository,
|
private val tunnelRepository: TunnelRepository,
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
|
private val appVisibilityObserver: AppVisibilityObserver,
|
||||||
|
private val globalEffectRepository: GlobalEffectRepository,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class)
|
||||||
@@ -36,54 +44,174 @@ class TunnelEventDispatcher(
|
|||||||
tunnelDisplayStates: StateFlow<Map<Int, DisplayTunnelState>>,
|
tunnelDisplayStates: StateFlow<Map<Int, DisplayTunnelState>>,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// informational events
|
// Informational events from tunnel backend
|
||||||
providerEvents
|
providerEvents
|
||||||
.distinctUntilChanged()
|
|
||||||
.onEach { event ->
|
.onEach { event ->
|
||||||
when (event) {
|
when (event) {
|
||||||
is TunnelEvent.FallbackToIpv4 -> {
|
is TunnelEvent.FallbackToIpv4 -> {
|
||||||
val name = getTunnelName(event.tunnelId)
|
val name = getTunnelName(event.tunnelId)
|
||||||
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 -> {
|
is TunnelEvent.RecoveredToIpv6 -> {
|
||||||
val name = getTunnelName(event.tunnelId)
|
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 -> {
|
is TunnelEvent.DynamicDnsUpdate -> {
|
||||||
val name = getTunnelName(event.tunnelId)
|
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 -> {
|
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)
|
.launchIn(scope)
|
||||||
|
|
||||||
// errors from the coordinator
|
// Errors from our tunnel coordinator
|
||||||
coordinatorErrors
|
coordinatorErrors
|
||||||
.distinctUntilChanged()
|
|
||||||
.onEach { error ->
|
.onEach { error ->
|
||||||
when (error) {
|
when (error) {
|
||||||
is TunnelErrorEvent.VpnPermissionDenied -> {
|
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 -> {
|
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 -> {
|
is TunnelErrorEvent.Socks5PortUnavailable -> {
|
||||||
val name = getTunnelName(error.tunnelId)
|
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 -> {
|
is TunnelErrorEvent.HttpPortUnavailable -> {
|
||||||
val name = getTunnelName(error.tunnelId)
|
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 tunnel = allTunnels.find { it.id == id } ?: return@mapNotNull null
|
||||||
|
|
||||||
val displayState =
|
val displayState =
|
||||||
displayStates[id]
|
displayStates[id] ?: DisplayTunnelState.from(activeTunnel)
|
||||||
?: DisplayTunnelState.from(activeTunnel, System.currentTimeMillis())
|
|
||||||
|
|
||||||
TunnelNotificationLine(
|
TunnelNotificationLine(
|
||||||
id = id,
|
id = id,
|
||||||
@@ -135,8 +262,7 @@ class TunnelEventDispatcher(
|
|||||||
|
|
||||||
val tunnel = allTunnels.find { it.id == id } ?: return@mapNotNull null
|
val tunnel = allTunnels.find { it.id == id } ?: return@mapNotNull null
|
||||||
val displayState =
|
val displayState =
|
||||||
displayStates[id]
|
displayStates[id] ?: DisplayTunnelState.from(activeTunnel)
|
||||||
?: DisplayTunnelState.from(activeTunnel, System.currentTimeMillis())
|
|
||||||
|
|
||||||
TunnelNotificationLine(
|
TunnelNotificationLine(
|
||||||
id = id,
|
id = id,
|
||||||
@@ -154,6 +280,18 @@ class TunnelEventDispatcher(
|
|||||||
.launchIn(scope)
|
.launchIn(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showOrNotify(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
foregroundAction: suspend () -> Unit,
|
||||||
|
backgroundAction: () -> Unit,
|
||||||
|
) {
|
||||||
|
if (appVisibilityObserver.isForeground.value) {
|
||||||
|
scope.launch { foregroundAction() }
|
||||||
|
} else {
|
||||||
|
backgroundAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun getTunnelName(tunnelId: Int): String {
|
private suspend fun getTunnelName(tunnelId: Int): String {
|
||||||
return tunnelRepository.getById(tunnelId)?.name ?: context.getString(R.string.unknown)
|
return tunnelRepository.getById(tunnelId)?.name ?: context.getString(R.string.unknown)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.orchestration
|
package com.zaneschepke.wireguardautotunnel.core.orchestration
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||||
|
|
||||||
class AutoTunnelCoordinator(
|
class AutoTunnelCoordinator(
|
||||||
private val repository: AutoTunnelSettingsRepository,
|
private val repository: AutoTunnelSettingsRepository,
|
||||||
|
|||||||
+36
-9
@@ -2,7 +2,6 @@ package com.zaneschepke.wireguardautotunnel.core.orchestration
|
|||||||
|
|
||||||
import com.zaneschepke.tunnel.model.BackendMode
|
import com.zaneschepke.tunnel.model.BackendMode
|
||||||
import com.zaneschepke.wireguardautotunnel.core.event.TunnelErrorEvent
|
import com.zaneschepke.wireguardautotunnel.core.event.TunnelErrorEvent
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.RoomDnsSettingsRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.RoomDnsSettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelActionSource
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelActionSource
|
||||||
@@ -10,13 +9,16 @@ import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
|||||||
import com.zaneschepke.wireguardautotunnel.domain.events.TunnelActionEvent
|
import com.zaneschepke.wireguardautotunnel.domain.events.TunnelActionEvent
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings
|
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
|
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.MonitoringSettings
|
import com.zaneschepke.wireguardautotunnel.domain.model.MonitoringSettings
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
|
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -44,6 +46,7 @@ class TunnelCoordinator(
|
|||||||
dnsSettingsRepository: RoomDnsSettingsRepository,
|
dnsSettingsRepository: RoomDnsSettingsRepository,
|
||||||
monitoringSettingsRepository: MonitoringSettingsRepository,
|
monitoringSettingsRepository: MonitoringSettingsRepository,
|
||||||
proxyRepository: ProxySettingsRepository,
|
proxyRepository: ProxySettingsRepository,
|
||||||
|
lockdownModeRepository: LockdownSettingsRepository,
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -55,7 +58,7 @@ class TunnelCoordinator(
|
|||||||
tunnelProvider.backendStatus
|
tunnelProvider.backendStatus
|
||||||
.map { status ->
|
.map { status ->
|
||||||
status.activeTunnels.mapValues { (_, activeTunnel) ->
|
status.activeTunnels.mapValues { (_, activeTunnel) ->
|
||||||
DisplayTunnelState.from(activeTunnel, System.currentTimeMillis())
|
DisplayTunnelState.from(activeTunnel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.debounce(400L.milliseconds)
|
.debounce(400L.milliseconds)
|
||||||
@@ -66,6 +69,7 @@ class TunnelCoordinator(
|
|||||||
val dns: DnsSettings,
|
val dns: DnsSettings,
|
||||||
val monitoring: MonitoringSettings,
|
val monitoring: MonitoringSettings,
|
||||||
val proxy: ProxySettings,
|
val proxy: ProxySettings,
|
||||||
|
val lockdown: LockdownSettings,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val runtimeSettingsSnapshot =
|
private val runtimeSettingsSnapshot =
|
||||||
@@ -74,12 +78,14 @@ class TunnelCoordinator(
|
|||||||
dnsSettingsRepository.flow,
|
dnsSettingsRepository.flow,
|
||||||
monitoringSettingsRepository.flow,
|
monitoringSettingsRepository.flow,
|
||||||
proxyRepository.flow,
|
proxyRepository.flow,
|
||||||
) { general, dns, monitoring, proxy ->
|
lockdownModeRepository.flow,
|
||||||
|
) { general, dns, monitoring, proxy, lockdown ->
|
||||||
RuntimeSettingsSnapshot(
|
RuntimeSettingsSnapshot(
|
||||||
general = general,
|
general = general,
|
||||||
dns = dns,
|
dns = dns,
|
||||||
monitoring = monitoring,
|
monitoring = monitoring,
|
||||||
proxy = proxy,
|
proxy = proxy,
|
||||||
|
lockdown = lockdown,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +123,7 @@ class TunnelCoordinator(
|
|||||||
|
|
||||||
// enforce single tunnel, for now
|
// enforce single tunnel, for now
|
||||||
if (backendStatus.value.activeTunnels.isNotEmpty()) {
|
if (backendStatus.value.activeTunnels.isNotEmpty()) {
|
||||||
stopActiveTunnelsInternal()
|
stopActiveTunnelsInternal(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
startTunnelInternal(config, source)
|
startTunnelInternal(config, source)
|
||||||
@@ -131,7 +137,13 @@ class TunnelCoordinator(
|
|||||||
stopTunnelInternal(id, source)
|
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(
|
private suspend fun startTunnelInternal(
|
||||||
tunnelConfig: TunnelConfig,
|
tunnelConfig: TunnelConfig,
|
||||||
@@ -143,6 +155,7 @@ class TunnelCoordinator(
|
|||||||
val dnsSettings = snapshot.dns
|
val dnsSettings = snapshot.dns
|
||||||
val proxySettings = snapshot.proxy
|
val proxySettings = snapshot.proxy
|
||||||
val monitoringSettings = snapshot.monitoring
|
val monitoringSettings = snapshot.monitoring
|
||||||
|
val lockdownSettings = snapshot.lockdown
|
||||||
|
|
||||||
val config = tunnelConfig.getConfig()
|
val config = tunnelConfig.getConfig()
|
||||||
val policy =
|
val policy =
|
||||||
@@ -178,8 +191,10 @@ class TunnelCoordinator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
TunnelMode.LOCK_DOWN -> {
|
TunnelMode.LOCK_DOWN -> {
|
||||||
|
BackendMode.Proxy.KillSwitchPrimary(
|
||||||
BackendMode.Proxy.KillSwitchPrimary(runConfig)
|
runConfig,
|
||||||
|
lockdownSettings.toKillSwitchConfig(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,6 +221,10 @@ class TunnelCoordinator(
|
|||||||
|
|
||||||
suspend fun toggleTunnels(source: TunnelActionSource = TunnelActionSource.USER) =
|
suspend fun toggleTunnels(source: TunnelActionSource = TunnelActionSource.USER) =
|
||||||
tunnelMutex.withLock {
|
tunnelMutex.withLock {
|
||||||
|
if (source == TunnelActionSource.USER) {
|
||||||
|
_userOverrideFlow.tryEmit(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
val active = tunnelProvider.backendStatus.value.activeTunnels
|
val active = tunnelProvider.backendStatus.value.activeTunnels
|
||||||
if (active.isNotEmpty()) {
|
if (active.isNotEmpty()) {
|
||||||
lastActiveTunnels = active.keys.toList()
|
lastActiveTunnels = active.keys.toList()
|
||||||
@@ -214,7 +233,7 @@ class TunnelCoordinator(
|
|||||||
_actions.emit(TunnelActionEvent.Stopped(tunnelId = id, source = source))
|
_actions.emit(TunnelActionEvent.Stopped(tunnelId = id, source = source))
|
||||||
}
|
}
|
||||||
|
|
||||||
stopActiveTunnelsInternal()
|
stopActiveTunnelsInternal(source)
|
||||||
return@withLock
|
return@withLock
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +258,15 @@ class TunnelCoordinator(
|
|||||||
.onFailure { _errors.emit(TunnelErrorEvent.from(it, id)) }
|
.onFailure { _errors.emit(TunnelErrorEvent.from(it, id)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun stopActiveTunnelsInternal() {
|
private suspend fun stopActiveTunnelsInternal(
|
||||||
|
source: TunnelActionSource = TunnelActionSource.USER
|
||||||
|
) {
|
||||||
|
val active = tunnelProvider.backendStatus.value.activeTunnels
|
||||||
|
|
||||||
|
active.keys.forEach { id ->
|
||||||
|
_actions.emit(TunnelActionEvent.Stopped(tunnelId = id, source = source))
|
||||||
|
}
|
||||||
|
|
||||||
tunnelProvider.stopActiveTunnels()
|
tunnelProvider.stopActiveTunnels()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-4
@@ -19,9 +19,8 @@ class ShortcutsActivity : ComponentActivity() {
|
|||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
applicationScope.launch {
|
finish()
|
||||||
shortcutCoordinator.handle(intent)
|
|
||||||
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.backend.Backend
|
||||||
import com.zaneschepke.tunnel.model.BackendMode
|
import com.zaneschepke.tunnel.model.BackendMode
|
||||||
import com.zaneschepke.tunnel.state.BackendStatus
|
import com.zaneschepke.tunnel.state.BackendStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings
|
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings
|
||||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
@@ -52,10 +49,4 @@ class TunnelBackendProvider(
|
|||||||
override suspend fun disableLockDown(): Result<Unit> {
|
override suspend fun disableLockDown(): Result<Unit> {
|
||||||
return backend.disableKillSwitch()
|
return backend.disableKillSwitch()
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
private val localErrorEvents = MutableSharedFlow<Pair<String?, BackendCoreException>>()
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
private val localMessageEvents = MutableSharedFlow<Pair<String?, BackendMessage>>()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import androidx.work.ExistingPeriodicWorkPolicy
|
|||||||
import androidx.work.PeriodicWorkRequestBuilder
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
|
|||||||
DnsSettings::class,
|
DnsSettings::class,
|
||||||
LockdownSettings::class,
|
LockdownSettings::class,
|
||||||
],
|
],
|
||||||
version = 30,
|
version = 31,
|
||||||
autoMigrations =
|
autoMigrations =
|
||||||
[
|
[
|
||||||
AutoMigration(from = 1, to = 2),
|
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 = 26, to = 27, spec = GlobalsMigration::class),
|
||||||
AutoMigration(from = 27, to = 28, spec = DonationMigration::class),
|
AutoMigration(from = 27, to = 28, spec = DonationMigration::class),
|
||||||
AutoMigration(from = 29, to = 30, spec = SingleConfigMigration::class),
|
AutoMigration(from = 29, to = 30, spec = SingleConfigMigration::class),
|
||||||
|
AutoMigration(from = 30, to = 31),
|
||||||
],
|
],
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
)
|
)
|
||||||
|
|||||||
+3
@@ -18,4 +18,7 @@ interface AutoTunnelSettingsDao {
|
|||||||
|
|
||||||
@Query("UPDATE auto_tunnel_settings SET is_tunnel_enabled = :enabled")
|
@Query("UPDATE auto_tunnel_settings SET is_tunnel_enabled = :enabled")
|
||||||
suspend fun updateAutoTunnelEnabled(enabled: Boolean)
|
suspend fun updateAutoTunnelEnabled(enabled: Boolean)
|
||||||
|
|
||||||
|
@Query("UPDATE auto_tunnel_settings SET disable_on_captive_portal = :enabled")
|
||||||
|
suspend fun updateDisableOnCaptivePortal(enabled: Boolean)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -27,4 +27,6 @@ data class AutoTunnelSettings(
|
|||||||
@ColumnInfo(name = "wifi_detection_method", defaultValue = "0")
|
@ColumnInfo(name = "wifi_detection_method", defaultValue = "0")
|
||||||
val wifiDetectionMethod: WifiDetectionMethod = WifiDetectionMethod.fromValue(0),
|
val wifiDetectionMethod: WifiDetectionMethod = WifiDetectionMethod.fromValue(0),
|
||||||
@ColumnInfo(name = "start_on_boot", defaultValue = "0") val startOnBoot: Boolean = false,
|
@ColumnInfo(name = "start_on_boot", defaultValue = "0") val startOnBoot: Boolean = false,
|
||||||
|
@ColumnInfo(name = "disable_on_captive_portal", defaultValue = "1")
|
||||||
|
val disableTunnelOnCaptivePortal: Boolean = true,
|
||||||
)
|
)
|
||||||
|
|||||||
+4
@@ -26,4 +26,8 @@ class RoomAutoTunnelSettingsRepository(private val autoTunnelSettingsDao: AutoTu
|
|||||||
override suspend fun updateAutoTunnelEnabled(enabled: Boolean) {
|
override suspend fun updateAutoTunnelEnabled(enabled: Boolean) {
|
||||||
autoTunnelSettingsDao.updateAutoTunnelEnabled(enabled)
|
autoTunnelSettingsDao.updateAutoTunnelEnabled(enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun updateDisableOnCaptivePortal(enabled: Boolean) {
|
||||||
|
autoTunnelSettingsDao.updateDisableOnCaptivePortal(enabled)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import android.os.StrictMode
|
|||||||
import com.zaneschepke.logcatter.LogReader
|
import com.zaneschepke.logcatter.LogReader
|
||||||
import com.zaneschepke.logcatter.LogcatReader
|
import com.zaneschepke.logcatter.LogcatReader
|
||||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.DynamicShortcutManager
|
import com.zaneschepke.wireguardautotunnel.core.shortcut.DynamicShortcutManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.SelectedTunnelsRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.SelectedTunnelsRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
|
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ val coordinatorModule = module {
|
|||||||
get(),
|
get(),
|
||||||
get(),
|
get(),
|
||||||
get(),
|
get(),
|
||||||
|
get(),
|
||||||
get(named(Scope.APPLICATION)),
|
get(named(Scope.APPLICATION)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,19 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.di
|
package com.zaneschepke.wireguardautotunnel.di
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.content.Context
|
|
||||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||||
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
||||||
import com.zaneschepke.tunnel.NotificationProvider
|
import com.zaneschepke.tunnel.ApplicationProvider
|
||||||
import com.zaneschepke.tunnel.backend.RootShell
|
import com.zaneschepke.tunnel.util.RootShell
|
||||||
import com.zaneschepke.tunnel.util.RootShellException
|
import com.zaneschepke.tunnel.util.RootShellException
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.event.TunnelEventDispatcher
|
import com.zaneschepke.wireguardautotunnel.core.event.TunnelEventDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService.NotificationChannels
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.AndroidApplicationProvider
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidTunnelNotificationService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.PROXY_GROUP_KEY
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.VPN_GROUP_KEY
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.TunnelNotificationService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelTileRefresher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelBackendProvider
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelBackendProvider
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.lifecyle.AppVisibilityObserver
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.AndroidTunnelNotificationService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationService
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.to
|
import com.zaneschepke.wireguardautotunnel.util.extensions.to
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -36,40 +30,15 @@ import timber.log.Timber
|
|||||||
|
|
||||||
val tunnelBackendProviderModule = module {
|
val tunnelBackendProviderModule = module {
|
||||||
single<TunnelNotificationService> { AndroidTunnelNotificationService(get()) }
|
single<TunnelNotificationService> { AndroidTunnelNotificationService(get()) }
|
||||||
|
single { AppVisibilityObserver() }
|
||||||
singleOf(::TunnelEventDispatcher)
|
singleOf(::TunnelEventDispatcher)
|
||||||
|
|
||||||
single<NotificationProvider> {
|
single<ApplicationProvider> {
|
||||||
val notificationService = get<NotificationService>()
|
AndroidApplicationProvider(
|
||||||
val context = androidContext()
|
notificationService = get(),
|
||||||
object : NotificationProvider {
|
tunnelNotificationService = get(),
|
||||||
override val vpnInitNotification: Notification
|
tunnelRepository = get(),
|
||||||
get() =
|
)
|
||||||
notificationService.createNotification(
|
|
||||||
channel = NotificationChannels.Tunnel.VPN,
|
|
||||||
title = context.getString(R.string.initializing),
|
|
||||||
onGoing = true,
|
|
||||||
groupKey = VPN_GROUP_KEY,
|
|
||||||
)
|
|
||||||
|
|
||||||
override val proxyInitNotification: Notification
|
|
||||||
get() =
|
|
||||||
notificationService.createNotification(
|
|
||||||
channel = NotificationChannels.Tunnel.Proxy,
|
|
||||||
title = context.getString(R.string.initializing),
|
|
||||||
onGoing = true,
|
|
||||||
groupKey = PROXY_GROUP_KEY,
|
|
||||||
)
|
|
||||||
|
|
||||||
override val vpnNotificationId: Int
|
|
||||||
get() = NotificationService.VPN_NOTIFICATION_ID
|
|
||||||
|
|
||||||
override val proxyNotificationId: Int
|
|
||||||
get() = NotificationService.PROXY_NOTIFICATION_ID
|
|
||||||
|
|
||||||
override fun refreshTile(context: Context) {
|
|
||||||
TunnelTileRefresher.refresh(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
single {
|
single {
|
||||||
|
|||||||
+1
@@ -14,4 +14,5 @@ data class AutoTunnelSettings(
|
|||||||
val isTunnelOnUnsecureEnabled: Boolean = false,
|
val isTunnelOnUnsecureEnabled: Boolean = false,
|
||||||
val wifiDetectionMethod: WifiDetectionMethod = WifiDetectionMethod.fromValue(0),
|
val wifiDetectionMethod: WifiDetectionMethod = WifiDetectionMethod.fromValue(0),
|
||||||
val startOnBoot: Boolean = false,
|
val startOnBoot: Boolean = false,
|
||||||
|
val disableTunnelOnCaptivePortal: Boolean = true,
|
||||||
)
|
)
|
||||||
|
|||||||
+2
@@ -11,4 +11,6 @@ interface AutoTunnelSettingsRepository {
|
|||||||
suspend fun getAutoTunnelSettings(): AutoTunnelSettings
|
suspend fun getAutoTunnelSettings(): AutoTunnelSettings
|
||||||
|
|
||||||
suspend fun updateAutoTunnelEnabled(enabled: Boolean)
|
suspend fun updateAutoTunnelEnabled(enabled: Boolean)
|
||||||
|
|
||||||
|
suspend fun updateDisableOnCaptivePortal(enabled: Boolean)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.asSharedFlow
|
|||||||
class GlobalEffectRepository {
|
class GlobalEffectRepository {
|
||||||
|
|
||||||
private val _globalEffectFlow =
|
private val _globalEffectFlow =
|
||||||
MutableSharedFlow<GlobalSideEffect>(replay = 0, extraBufferCapacity = 1)
|
MutableSharedFlow<GlobalSideEffect>(replay = 0, extraBufferCapacity = 0)
|
||||||
val flow = _globalEffectFlow.asSharedFlow()
|
val flow = _globalEffectFlow.asSharedFlow()
|
||||||
|
|
||||||
suspend fun post(effect: GlobalSideEffect) {
|
suspend fun post(effect: GlobalSideEffect) {
|
||||||
|
|||||||
+2
-4
@@ -1,8 +1,8 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.domain.sideeffect
|
package com.zaneschepke.wireguardautotunnel.domain.sideeffect
|
||||||
|
|
||||||
|
import com.dokar.sonner.ToastType
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarType
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@@ -10,14 +10,12 @@ sealed class GlobalSideEffect {
|
|||||||
|
|
||||||
data class Snackbar(
|
data class Snackbar(
|
||||||
val message: StringValue,
|
val message: StringValue,
|
||||||
val type: SnackbarType? = null,
|
val type: ToastType,
|
||||||
val actionLabel: String? = null,
|
val actionLabel: String? = null,
|
||||||
val onAction: (() -> Unit)? = null,
|
val onAction: (() -> Unit)? = null,
|
||||||
val durationMs: Long? = null,
|
val durationMs: Long? = null,
|
||||||
) : GlobalSideEffect()
|
) : GlobalSideEffect()
|
||||||
|
|
||||||
data class Toast(val message: StringValue) : GlobalSideEffect()
|
|
||||||
|
|
||||||
data object PopBackStack : GlobalSideEffect()
|
data object PopBackStack : GlobalSideEffect()
|
||||||
|
|
||||||
data class LaunchUrl(val url: String) : GlobalSideEffect()
|
data class LaunchUrl(val url: String) : GlobalSideEffect()
|
||||||
|
|||||||
@@ -11,16 +11,20 @@ sealed class ActiveNetwork {
|
|||||||
|
|
||||||
data object Cellular : 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(
|
data class NetworkState(
|
||||||
val activeNetwork: ActiveNetwork = ActiveNetwork.Disconnected,
|
val activeNetwork: ActiveNetwork = ActiveNetwork.Disconnected,
|
||||||
val locationServicesEnabled: Boolean = false,
|
val locationServicesEnabled: Boolean = false,
|
||||||
val locationPermissionGranted: Boolean = false,
|
val locationPermissionGranted: Boolean = false,
|
||||||
) {
|
// Has a network that can actually transfer data (not suspended)
|
||||||
fun hasInternet(): Boolean = activeNetwork !is ActiveNetwork.Disconnected
|
val hasUsableNetwork: Boolean = false,
|
||||||
}
|
)
|
||||||
|
|
||||||
fun ConnectivityState.toDomain(): NetworkState {
|
fun ConnectivityState.toDomain(): NetworkState {
|
||||||
val domainNetwork: ActiveNetwork =
|
val domainNetwork: ActiveNetwork =
|
||||||
@@ -33,7 +37,11 @@ fun ConnectivityState.toDomain(): NetworkState {
|
|||||||
null -> null
|
null -> null
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
ActiveNetwork.Wifi(ssid = network.ssid, isSecure = isSecure)
|
ActiveNetwork.Wifi(
|
||||||
|
ssid = network.ssid,
|
||||||
|
isSecure = isSecure,
|
||||||
|
requiresCaptivePortalLogin(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
is MonitorActiveNetwork.Cellular -> ActiveNetwork.Cellular
|
is MonitorActiveNetwork.Cellular -> ActiveNetwork.Cellular
|
||||||
is MonitorActiveNetwork.Ethernet -> ActiveNetwork.Ethernet
|
is MonitorActiveNetwork.Ethernet -> ActiveNetwork.Ethernet
|
||||||
@@ -44,5 +52,6 @@ fun ConnectivityState.toDomain(): NetworkState {
|
|||||||
activeNetwork = domainNetwork,
|
activeNetwork = domainNetwork,
|
||||||
locationPermissionGranted = this.locationPermissionsGranted,
|
locationPermissionGranted = this.locationPermissionsGranted,
|
||||||
locationServicesEnabled = this.locationServicesEnabled,
|
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.Manifest
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
@@ -17,8 +17,8 @@ import androidx.core.app.NotificationManagerCompat.IMPORTANCE_HIGH
|
|||||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
import com.zaneschepke.wireguardautotunnel.MainActivity
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.core.broadcast.NotificationActionReceiver
|
import com.zaneschepke.wireguardautotunnel.core.broadcast.NotificationActionReceiver
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.EXTRA_ID
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.EXTRA_ID
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
|
|
||||||
class AndroidNotificationService(override val context: Context) : NotificationService {
|
class AndroidNotificationService(override val context: Context) : NotificationService {
|
||||||
+130
-25
@@ -1,21 +1,116 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
package com.zaneschepke.wireguardautotunnel.notification
|
||||||
|
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService.NotificationChannels
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.PROXY_GROUP_KEY
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.PROXY_NOTIFICATION_ID
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.TUNNEL_ERROR_NOTIFICATION_ID
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.TUNNEL_MESSAGES_NOTIFICATION_ID
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.VPN_GROUP_KEY
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.VPN_NOTIFICATION_ID
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService.NotificationChannels
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.PROXY_GROUP_KEY
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.PROXY_NOTIFICATION_ID
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.TUNNEL_ERROR_NOTIFICATION_ID
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.TUNNEL_MESSAGES_NOTIFICATION_ID
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.VPN_GROUP_KEY
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.VPN_NOTIFICATION_ID
|
||||||
|
|
||||||
class AndroidTunnelNotificationService(private val notificationService: NotificationService) :
|
class AndroidTunnelNotificationService(private val notificationService: NotificationService) :
|
||||||
TunnelNotificationService {
|
TunnelNotificationService {
|
||||||
|
|
||||||
private val context = notificationService.context
|
private val context = notificationService.context
|
||||||
|
|
||||||
|
private fun createGroupNotification(
|
||||||
|
tunnelNotificationLines: Map<Int, TunnelNotificationLine>,
|
||||||
|
channel: NotificationChannels.Tunnel,
|
||||||
|
groupKey: String,
|
||||||
|
): android.app.Notification {
|
||||||
|
val title =
|
||||||
|
if (tunnelNotificationLines.size == 1) {
|
||||||
|
val name = tunnelNotificationLines.values.first().name
|
||||||
|
when (channel) {
|
||||||
|
is NotificationChannels.Tunnel.VPN ->
|
||||||
|
"${context.getString(R.string.vpn)} • $name"
|
||||||
|
is NotificationChannels.Tunnel.Proxy ->
|
||||||
|
"${context.getString(R.string.proxy)} • $name"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
when (channel) {
|
||||||
|
is NotificationChannels.Tunnel.VPN -> context.getString(R.string.vpn)
|
||||||
|
is NotificationChannels.Tunnel.Proxy -> context.getString(R.string.proxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val formattedLines =
|
||||||
|
tunnelNotificationLines.values.map { line ->
|
||||||
|
val status = line.displayState.asLocalizedString(context)
|
||||||
|
|
||||||
|
if (tunnelNotificationLines.size == 1) {
|
||||||
|
status
|
||||||
|
} else {
|
||||||
|
context.getString(R.string.notification_tunnel_status_format, line.name, status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val description = formattedLines.joinToString("\n")
|
||||||
|
|
||||||
|
val actions =
|
||||||
|
if (tunnelNotificationLines.size == 1) {
|
||||||
|
val tunnelId = tunnelNotificationLines.keys.first()
|
||||||
|
listOf(
|
||||||
|
notificationService.createNotificationAction(
|
||||||
|
notificationAction = NotificationAction.TUNNEL_OFF,
|
||||||
|
extraId = tunnelId,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
listOf(
|
||||||
|
notificationService.createNotificationAction(
|
||||||
|
notificationAction = NotificationAction.STOP_ALL,
|
||||||
|
extraId = null,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val style =
|
||||||
|
if (tunnelNotificationLines.size > 1) {
|
||||||
|
NotificationCompat.InboxStyle()
|
||||||
|
.setBigContentTitle(title)
|
||||||
|
.setSummaryText(
|
||||||
|
"${tunnelNotificationLines.size} ${context.getString(R.string.tunnels).lowercase()}"
|
||||||
|
)
|
||||||
|
.also { inbox -> formattedLines.forEach { inbox.addLine(it) } }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
return notificationService.createNotification(
|
||||||
|
channel = channel,
|
||||||
|
title = title,
|
||||||
|
description = description,
|
||||||
|
actions = actions,
|
||||||
|
onGoing = true,
|
||||||
|
onlyAlertOnce = true,
|
||||||
|
groupKey = groupKey,
|
||||||
|
style = style,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun buildVpnPersistentNotification(
|
||||||
|
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||||
|
): android.app.Notification {
|
||||||
|
return createGroupNotification(
|
||||||
|
tunnelNotificationLines,
|
||||||
|
NotificationChannels.Tunnel.VPN,
|
||||||
|
VPN_GROUP_KEY,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun buildProxyPersistentNotification(
|
||||||
|
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||||
|
): android.app.Notification {
|
||||||
|
return createGroupNotification(
|
||||||
|
tunnelNotificationLines,
|
||||||
|
NotificationChannels.Tunnel.Proxy,
|
||||||
|
PROXY_GROUP_KEY,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateGroupNotification(
|
private fun updateGroupNotification(
|
||||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>,
|
tunnelNotificationLines: Map<Int, TunnelNotificationLine>,
|
||||||
notificationId: Int,
|
notificationId: Int,
|
||||||
@@ -88,26 +183,36 @@ class AndroidTunnelNotificationService(private val notificationService: Notifica
|
|||||||
notificationService.show(notificationId, notification)
|
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(
|
override fun updateVpnPersistentNotification(
|
||||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||||
) {
|
) {
|
||||||
updateGroupNotification(
|
if (tunnelNotificationLines.isEmpty()) {
|
||||||
tunnelNotificationLines = tunnelNotificationLines,
|
notificationService.remove(VPN_NOTIFICATION_ID)
|
||||||
notificationId = VPN_NOTIFICATION_ID,
|
return
|
||||||
channel = NotificationChannels.Tunnel.VPN,
|
}
|
||||||
groupKey = VPN_GROUP_KEY,
|
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) {
|
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.app.Notification
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService.NotificationChannels
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService.NotificationChannels
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
|
|
||||||
interface NotificationService {
|
interface NotificationService {
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
package com.zaneschepke.wireguardautotunnel.notification
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
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 {
|
interface TunnelNotificationService {
|
||||||
|
|
||||||
@@ -6,6 +8,14 @@ interface TunnelNotificationService {
|
|||||||
|
|
||||||
fun updateVpnPersistentNotification(tunnelNotificationLines: Map<Int, TunnelNotificationLine>)
|
fun updateVpnPersistentNotification(tunnelNotificationLines: Map<Int, TunnelNotificationLine>)
|
||||||
|
|
||||||
|
fun buildVpnPersistentNotification(
|
||||||
|
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||||
|
): Notification
|
||||||
|
|
||||||
|
fun buildProxyPersistentNotification(
|
||||||
|
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||||
|
): Notification
|
||||||
|
|
||||||
fun showIpv4Fallback(tunnelName: String)
|
fun showIpv4Fallback(tunnelName: String)
|
||||||
|
|
||||||
fun showIpv6Recovery(tunnelName: String)
|
fun showIpv6Recovery(tunnelName: String)
|
||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.service
|
package com.zaneschepke.wireguardautotunnel.service
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
|
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelService
|
||||||
|
|
||||||
class ServiceManager(private val context: Context) {
|
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.events.AutoTunnelEvent
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||||
@@ -28,7 +28,19 @@ class AutoTunnelEngine {
|
|||||||
|
|
||||||
val activeTunnelIds = backend.activeTunnels.keys.toSet()
|
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) {
|
return if (settings.isStopOnNoInternetEnabled) {
|
||||||
Decision.StopDueToNoInternet
|
Decision.StopDueToNoInternet
|
||||||
} else {
|
} 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 android.content.Intent
|
||||||
import androidx.core.app.ServiceCompat
|
import androidx.core.app.ServiceCompat
|
||||||
@@ -7,10 +7,7 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||||
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelTileRefresher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.Dispatcher
|
import com.zaneschepke.wireguardautotunnel.di.Dispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelActionSource
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelActionSource
|
||||||
@@ -23,17 +20,21 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepos
|
|||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
|
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.toDomain
|
import com.zaneschepke.wireguardautotunnel.domain.state.toDomain
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelTileRefresher
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.to
|
import com.zaneschepke.wireguardautotunnel.util.extensions.to
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.mapNotNull
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
@@ -77,13 +78,16 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
@Volatile private var hasUserOverride = false
|
@Volatile private var hasUserOverride = false
|
||||||
private var lastNetworkFingerprint: AutoTunnelState.NetworkFingerprint? = null
|
private var lastNetworkFingerprint: AutoTunnelState.NetworkFingerprint? = null
|
||||||
|
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
private val autoTunnelStateFlow: Flow<AutoTunnelState> by lazy {
|
private val autoTunnelStateFlow: Flow<AutoTunnelState> by lazy {
|
||||||
val networkFlow = networkEngine.stableState.mapNotNull { it?.state?.toDomain() }
|
val networkFlow = networkEngine.stableState.mapNotNull { it?.state?.toDomain() }
|
||||||
|
|
||||||
val settingsFlow = combineSettings()
|
val settingsFlow = combineSettings()
|
||||||
|
|
||||||
val backendFlow =
|
val backendFlow =
|
||||||
tunnelCoordinator.backendStatus.distinctUntilChangedBy { it.activeTunnels.keys.toSet() }
|
tunnelCoordinator.backendStatus
|
||||||
|
.distinctUntilChanged { old, new -> old.activeTunnels == new.activeTunnels }
|
||||||
|
.debounce(300L.milliseconds)
|
||||||
|
|
||||||
combine(networkFlow, settingsFlow, backendFlow) { network, settings, backend ->
|
combine(networkFlow, settingsFlow, backendFlow) { network, settings, backend ->
|
||||||
AutoTunnelState(
|
AutoTunnelState(
|
||||||
@@ -188,11 +192,11 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
reconciliationMutex.withLock {
|
reconciliationMutex.withLock {
|
||||||
val currentNetworkState = networkEngine.stableState.value?.state?.toDomain()
|
val currentNetworkState = networkEngine.stableState.value?.state?.toDomain()
|
||||||
|
|
||||||
val stillNoInternet = currentNetworkState?.hasInternet() == false
|
val stillNoUsableNetwork = currentNetworkState?.hasUsableNetwork == false
|
||||||
val stopOnNoInternetEnabled =
|
val stopOnNoInternetEnabled =
|
||||||
autoTunnelRepository.flow.firstOrNull()?.isStopOnNoInternetEnabled == true
|
autoTunnelRepository.flow.firstOrNull()?.isStopOnNoInternetEnabled == true
|
||||||
|
|
||||||
if (stillNoInternet && stopOnNoInternetEnabled) {
|
if (stillNoUsableNetwork && stopOnNoInternetEnabled) {
|
||||||
val currentActiveIds =
|
val currentActiveIds =
|
||||||
tunnelCoordinator.backendStatus.value.activeTunnels.keys
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
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.Tile
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
package com.zaneschepke.wireguardautotunnel.service.tile
|
||||||
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
package com.zaneschepke.wireguardautotunnel.service.tile
|
||||||
|
|
||||||
import android.content.Context
|
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.os.Build
|
||||||
import android.service.quicksettings.Tile
|
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.ComponentName
|
||||||
import android.content.Context
|
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.Icons
|
||||||
import androidx.compose.material.icons.outlined.Filter1
|
import androidx.compose.material.icons.outlined.Filter1
|
||||||
import androidx.compose.material.icons.outlined.Map
|
import androidx.compose.material.icons.outlined.Map
|
||||||
|
import androidx.compose.material.icons.outlined.PublicOff
|
||||||
import androidx.compose.material.icons.outlined.WifiFind
|
import androidx.compose.material.icons.outlined.WifiFind
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -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 {
|
Column {
|
||||||
GroupLabel(stringResource(R.string.tunnels), Modifier.padding(horizontal = 16.dp))
|
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.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import com.dokar.sonner.ToastType
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||||
@@ -45,11 +46,17 @@ fun PinLockScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
|||||||
textColor = MaterialTheme.colorScheme.onSurface,
|
textColor = MaterialTheme.colorScheme.onSurface,
|
||||||
onPinCorrect = { onPinCorrect() },
|
onPinCorrect = { onPinCorrect() },
|
||||||
onPinIncorrect = {
|
onPinIncorrect = {
|
||||||
sharedViewModel.showToast(StringValue.StringResource(R.string.incorrect_pin))
|
sharedViewModel.showSnackMessage(
|
||||||
|
StringValue.StringResource(R.string.incorrect_pin),
|
||||||
|
ToastType.Warning,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onPinCreated = {
|
onPinCreated = {
|
||||||
pinCreated = true
|
pinCreated = true
|
||||||
sharedViewModel.showToast(StringValue.StringResource(R.string.pin_created))
|
sharedViewModel.showSnackMessage(
|
||||||
|
StringValue.StringResource(R.string.pin_created),
|
||||||
|
ToastType.Success,
|
||||||
|
)
|
||||||
sharedViewModel.setPinLockEnabled(true)
|
sharedViewModel.setPinLockEnabled(true)
|
||||||
onPinCorrect()
|
onPinCorrect()
|
||||||
},
|
},
|
||||||
|
|||||||
+49
-10
@@ -38,6 +38,7 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.text.intl.Locale
|
import androidx.compose.ui.text.intl.Locale
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.dokar.sonner.ToastType
|
||||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
import com.zaneschepke.wireguardautotunnel.MainActivity
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||||
@@ -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.common.text.DescriptionText
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackupBottomSheet
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackupBottomSheet
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackupEncryptionDialog
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.proxy.compoents.AppModeBottomSheet
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.proxy.compoents.AppModeBottomSheet
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asString
|
import com.zaneschepke.wireguardautotunnel.util.extensions.asString
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString
|
import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.capitalize
|
import com.zaneschepke.wireguardautotunnel.util.extensions.capitalize
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
@@ -88,6 +89,9 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var showBackupSheet by rememberSaveable { mutableStateOf(false) }
|
var showBackupSheet by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var showEncryptionDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var isRestoreAction by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
var showAppModeSheet by rememberSaveable { mutableStateOf(false) }
|
var showAppModeSheet by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
val appMode = uiState.settings.tunnelMode
|
val appMode = uiState.settings.tunnelMode
|
||||||
@@ -99,19 +103,53 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun performBackupRestore(action: () -> Unit) {
|
fun performBackupRestore(action: () -> Unit) {
|
||||||
if (uiState.tunnelActive || globalUiState.isAutoTunnelActive)
|
|
||||||
return context.showToast(R.string.all_services_disabled)
|
|
||||||
showBackupSheet = false
|
showBackupSheet = false
|
||||||
|
if (uiState.tunnelActive || globalUiState.isAutoTunnelActive) {
|
||||||
|
sharedViewModel.showSnackMessage(
|
||||||
|
StringValue.StringResource(R.string.all_services_disabled),
|
||||||
|
ToastType.Warning,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
action()
|
action()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showBackupSheet)
|
if (showBackupSheet) {
|
||||||
BackupBottomSheet(
|
BackupBottomSheet(
|
||||||
{ performBackupRestore { (context as? MainActivity)?.performBackup() } },
|
onBackup = {
|
||||||
{ performBackupRestore { (context as? MainActivity)?.performRestore() } },
|
showBackupSheet = false
|
||||||
) {
|
isRestoreAction = false
|
||||||
showBackupSheet = 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)
|
if (showAppModeSheet)
|
||||||
AppModeBottomSheet(sharedViewModel::setAppMode, uiState.settings.tunnelMode) {
|
AppModeBottomSheet(sharedViewModel::setAppMode, uiState.settings.tunnelMode) {
|
||||||
showAppModeSheet = false
|
showAppModeSheet = false
|
||||||
@@ -168,7 +206,8 @@ fun SettingsScreen(
|
|||||||
StringValue.StringResource(
|
StringValue.StringResource(
|
||||||
R.string.mode_disabled_template,
|
R.string.mode_disabled_template,
|
||||||
appMode.asString(context),
|
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.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import com.dokar.sonner.ToastType
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.components.LogList
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.components.LogList
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.components.LogsBottomSheet
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.components.LogsBottomSheet
|
||||||
@@ -86,13 +87,15 @@ fun LogsScreen(
|
|||||||
},
|
},
|
||||||
onCanceled = {
|
onCanceled = {
|
||||||
sharedViewModel.showSnackMessage(
|
sharedViewModel.showSnackMessage(
|
||||||
StringValue.StringResource(R.string.export_canceled)
|
StringValue.StringResource(R.string.export_canceled),
|
||||||
|
ToastType.Warning,
|
||||||
)
|
)
|
||||||
showLogsSheet = false
|
showLogsSheet = false
|
||||||
},
|
},
|
||||||
onUnsupported = {
|
onUnsupported = {
|
||||||
sharedViewModel.showSnackMessage(
|
sharedViewModel.showSnackMessage(
|
||||||
StringValue.StringResource(R.string.export_unsupported)
|
StringValue.StringResource(R.string.export_unsupported),
|
||||||
|
ToastType.Warning,
|
||||||
)
|
)
|
||||||
showLogsSheet = false
|
showLogsSheet = false
|
||||||
},
|
},
|
||||||
|
|||||||
+67
-15
@@ -26,6 +26,7 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -37,8 +38,10 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.dokar.sonner.ToastType
|
||||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
|
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
|
||||||
@@ -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.PermissionDialog
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.UpdateDialog
|
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.UpdateDialog
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchPlayStoreListing
|
import com.zaneschepke.wireguardautotunnel.util.extensions.launchPlayStoreListing
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchPlayStoreReview
|
import com.zaneschepke.wireguardautotunnel.util.extensions.launchPlayStoreReview
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchSupportEmail
|
import com.zaneschepke.wireguardautotunnel.util.extensions.launchSupportEmail
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SupportViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.SupportViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import org.orbitmvi.orbit.compose.collectAsState
|
import org.orbitmvi.orbit.compose.collectAsState
|
||||||
|
|
||||||
@@ -63,11 +67,24 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val isTv = LocalIsAndroidTV.current
|
val isTv = LocalIsAndroidTV.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
val supportState by viewModel.collectAsState()
|
val supportState by viewModel.collectAsState()
|
||||||
|
|
||||||
val clipboardManager = rememberClipboardHelper()
|
val clipboardManager = rememberClipboardHelper()
|
||||||
|
|
||||||
|
val issuesUrl = stringResource(R.string.github_url)
|
||||||
|
val izzyUrl = stringResource(R.string.fdroid_url)
|
||||||
|
val telegramUrl = stringResource(R.string.telegram_url)
|
||||||
|
val matrixUrl = stringResource(R.string.matrix_url)
|
||||||
|
val docsUrl = stringResource(R.string.docs_url)
|
||||||
|
val websiteUrl = stringResource(R.string.website_url)
|
||||||
|
val translationUrl = stringResource(R.string.translation_url)
|
||||||
|
val privacyPolicyUrl = stringResource(R.string.privacy_policy_url)
|
||||||
|
val playStoreUrl = "https://play.google.com/store/apps/details?id=${context.packageName}"
|
||||||
|
val playReviewsUrl =
|
||||||
|
"https://play.google.com/store/apps/details?id=${context.packageName}&showAllReviews=true"
|
||||||
|
|
||||||
val version = remember {
|
val version = remember {
|
||||||
"v${BuildConfig.VERSION_NAME +
|
"v${BuildConfig.VERSION_NAME +
|
||||||
if(BuildConfig.DEBUG) "-debug" else "" }"
|
if(BuildConfig.DEBUG) "-debug" else "" }"
|
||||||
@@ -95,6 +112,19 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
|||||||
PermissionDialog(context = context, onDismiss = { showPermissionDialog = false })
|
PermissionDialog(context = context, onDismiss = { showPermissionDialog = false })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun openWebUrl(url: String) {
|
||||||
|
context.openWebUrl(url).onFailure {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.postSideEffect(
|
||||||
|
GlobalSideEffect.Snackbar(
|
||||||
|
StringValue.StringResource(R.string.no_browser_detected),
|
||||||
|
ToastType.Error,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
|
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
@@ -115,19 +145,19 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
|||||||
)
|
)
|
||||||
SurfaceRow(
|
SurfaceRow(
|
||||||
stringResource(R.string.docs_description),
|
stringResource(R.string.docs_description),
|
||||||
onClick = { context.openWebUrl(context.getString(R.string.docs_url)) },
|
onClick = { openWebUrl(docsUrl) },
|
||||||
leading = { Icon(Icons.Outlined.Book, contentDescription = null) },
|
leading = { Icon(Icons.Outlined.Book, contentDescription = null) },
|
||||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||||
)
|
)
|
||||||
SurfaceRow(
|
SurfaceRow(
|
||||||
stringResource(R.string.website),
|
stringResource(R.string.website),
|
||||||
onClick = { context.openWebUrl(context.getString(R.string.website_url)) },
|
onClick = { openWebUrl(websiteUrl) },
|
||||||
leading = { Icon(Icons.Outlined.Web, contentDescription = null) },
|
leading = { Icon(Icons.Outlined.Web, contentDescription = null) },
|
||||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||||
)
|
)
|
||||||
SurfaceRow(
|
SurfaceRow(
|
||||||
stringResource(R.string.translation),
|
stringResource(R.string.translation),
|
||||||
onClick = { context.openWebUrl(context.getString(R.string.translation_url)) },
|
onClick = { openWebUrl(translationUrl) },
|
||||||
description = { DescriptionText(stringResource(R.string.help_translate)) },
|
description = { DescriptionText(stringResource(R.string.help_translate)) },
|
||||||
leading = { Icon(Icons.Outlined.Translate, contentDescription = null) },
|
leading = { Icon(Icons.Outlined.Translate, contentDescription = null) },
|
||||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||||
@@ -141,14 +171,16 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
|||||||
leading = { Icon(Icons.Outlined.Policy, contentDescription = null) },
|
leading = { Icon(Icons.Outlined.Policy, contentDescription = null) },
|
||||||
title = stringResource(R.string.privacy_policy),
|
title = stringResource(R.string.privacy_policy),
|
||||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||||
onClick = { context.openWebUrl(context.getString(R.string.privacy_policy_url)) },
|
onClick = { openWebUrl(privacyPolicyUrl) },
|
||||||
)
|
)
|
||||||
if (BuildConfig.FLAVOR == Constants.GOOGLE_PLAY_FLAVOR) {
|
if (BuildConfig.FLAVOR == Constants.GOOGLE_PLAY_FLAVOR) {
|
||||||
SurfaceRow(
|
SurfaceRow(
|
||||||
leading = { Icon(Icons.Outlined.Reviews, contentDescription = null) },
|
leading = { Icon(Icons.Outlined.Reviews, contentDescription = null) },
|
||||||
title = stringResource(R.string.review),
|
title = stringResource(R.string.review),
|
||||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||||
onClick = { context.launchPlayStoreReview() },
|
onClick = {
|
||||||
|
context.launchPlayStoreReview().onFailure { openWebUrl(playReviewsUrl) }
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,7 +199,7 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
|||||||
},
|
},
|
||||||
title = stringResource(R.string.join_matrix),
|
title = stringResource(R.string.join_matrix),
|
||||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||||
onClick = { context.openWebUrl(context.getString(R.string.matrix_url)) },
|
onClick = { openWebUrl(matrixUrl) },
|
||||||
)
|
)
|
||||||
SurfaceRow(
|
SurfaceRow(
|
||||||
leading = {
|
leading = {
|
||||||
@@ -179,7 +211,7 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
|||||||
},
|
},
|
||||||
title = stringResource(R.string.join_telegram),
|
title = stringResource(R.string.join_telegram),
|
||||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||||
onClick = { context.openWebUrl(context.getString(R.string.telegram_url)) },
|
onClick = { openWebUrl(telegramUrl) },
|
||||||
)
|
)
|
||||||
SurfaceRow(
|
SurfaceRow(
|
||||||
leading = {
|
leading = {
|
||||||
@@ -191,13 +223,24 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
|||||||
},
|
},
|
||||||
title = stringResource(R.string.open_issue),
|
title = stringResource(R.string.open_issue),
|
||||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||||
onClick = { context.openWebUrl(context.getString(R.string.github_url)) },
|
onClick = { openWebUrl(issuesUrl) },
|
||||||
)
|
)
|
||||||
SurfaceRow(
|
SurfaceRow(
|
||||||
leading = { Icon(Icons.Outlined.Mail, contentDescription = null) },
|
leading = { Icon(Icons.Outlined.Mail, contentDescription = null) },
|
||||||
title = stringResource(R.string.email_description),
|
title = stringResource(R.string.email_description),
|
||||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||||
onClick = { context.launchSupportEmail() },
|
onClick = {
|
||||||
|
context.launchSupportEmail().onFailure {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.postSideEffect(
|
||||||
|
GlobalSideEffect.Snackbar(
|
||||||
|
StringValue.StringResource(R.string.no_email_detected),
|
||||||
|
ToastType.Error,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Column {
|
Column {
|
||||||
@@ -222,12 +265,21 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
|||||||
leading = { Icon(Icons.Outlined.InstallMobile, contentDescription = null) },
|
leading = { Icon(Icons.Outlined.InstallMobile, contentDescription = null) },
|
||||||
title = stringResource(R.string.check_for_update),
|
title = stringResource(R.string.check_for_update),
|
||||||
onClick = {
|
onClick = {
|
||||||
if (BuildConfig.DEBUG)
|
if (BuildConfig.DEBUG) {
|
||||||
return@SurfaceRow context.showToast(R.string.update_check_unsupported)
|
scope.launch {
|
||||||
|
viewModel.postSideEffect(
|
||||||
|
GlobalSideEffect.Snackbar(
|
||||||
|
StringValue.StringResource(R.string.update_check_unsupported),
|
||||||
|
ToastType.Warning,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return@SurfaceRow
|
||||||
|
}
|
||||||
when (BuildConfig.FLAVOR) {
|
when (BuildConfig.FLAVOR) {
|
||||||
Constants.GOOGLE_PLAY_FLAVOR -> context.launchPlayStoreListing()
|
Constants.GOOGLE_PLAY_FLAVOR ->
|
||||||
Constants.FDROID_FLAVOR ->
|
context.launchPlayStoreListing().onFailure { openWebUrl(playStoreUrl) }
|
||||||
context.openWebUrl(context.getString(R.string.fdroid_url))
|
Constants.FDROID_FLAVOR -> openWebUrl(izzyUrl)
|
||||||
else -> viewModel.checkForStandaloneUpdate()
|
else -> viewModel.checkForStandaloneUpdate()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+17
-7
@@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.dokar.sonner.ToastType
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||||
@@ -50,11 +51,15 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
|||||||
rememberFileExportLauncherForResult(
|
rememberFileExportLauncherForResult(
|
||||||
onSuccess = { uri -> sharedViewModel.exportSelectedTunnels(uri) },
|
onSuccess = { uri -> sharedViewModel.exportSelectedTunnels(uri) },
|
||||||
onCanceled = {
|
onCanceled = {
|
||||||
sharedViewModel.showToast(StringValue.StringResource(R.string.export_canceled))
|
sharedViewModel.showSnackMessage(
|
||||||
|
StringValue.StringResource(R.string.export_canceled),
|
||||||
|
ToastType.Warning,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onUnsupported = {
|
onUnsupported = {
|
||||||
sharedViewModel.showSnackMessage(
|
sharedViewModel.showSnackMessage(
|
||||||
StringValue.StringResource(R.string.export_unsupported)
|
StringValue.StringResource(R.string.export_unsupported),
|
||||||
|
ToastType.Warning,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -73,7 +78,8 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
|||||||
selectedTunnelsExportLauncher.launch(fileName)
|
selectedTunnelsExportLauncher.launch(fileName)
|
||||||
} else {
|
} else {
|
||||||
sharedViewModel.showSnackMessage(
|
sharedViewModel.showSnackMessage(
|
||||||
StringValue.StringResource(R.string.error_no_file_explorer)
|
StringValue.StringResource(R.string.error_no_file_explorer),
|
||||||
|
ToastType.Error,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,7 +93,8 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
|||||||
rememberFileImportLauncherForResult(
|
rememberFileImportLauncherForResult(
|
||||||
onNoFileExplorer = {
|
onNoFileExplorer = {
|
||||||
sharedViewModel.showSnackMessage(
|
sharedViewModel.showSnackMessage(
|
||||||
StringValue.StringResource(R.string.error_no_file_explorer)
|
StringValue.StringResource(R.string.error_no_file_explorer),
|
||||||
|
ToastType.Error,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onData = { data -> sharedViewModel.importFromUri(data) },
|
onData = { data -> sharedViewModel.importFromUri(data) },
|
||||||
@@ -101,13 +108,15 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
|||||||
}
|
}
|
||||||
QRResult.QRMissingPermission -> {
|
QRResult.QRMissingPermission -> {
|
||||||
sharedViewModel.showSnackMessage(
|
sharedViewModel.showSnackMessage(
|
||||||
StringValue.StringResource(R.string.camera_permission_required)
|
StringValue.StringResource(R.string.camera_permission_required),
|
||||||
|
ToastType.Warning,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is QRResult.QRSuccess -> {
|
is QRResult.QRSuccess -> {
|
||||||
result.content.rawValue?.let { sharedViewModel.importFromQr(it) }
|
result.content.rawValue?.let { sharedViewModel.importFromQr(it) }
|
||||||
?: sharedViewModel.showSnackMessage(
|
?: sharedViewModel.showSnackMessage(
|
||||||
StringValue.StringResource(R.string.config_error)
|
StringValue.StringResource(R.string.config_error),
|
||||||
|
ToastType.Error,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
QRResult.QRUserCanceled -> Unit
|
QRResult.QRUserCanceled -> Unit
|
||||||
@@ -119,7 +128,8 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
|||||||
->
|
->
|
||||||
if (!isGranted) {
|
if (!isGranted) {
|
||||||
sharedViewModel.showSnackMessage(
|
sharedViewModel.showSnackMessage(
|
||||||
StringValue.StringResource(R.string.camera_permission_required)
|
StringValue.StringResource(R.string.camera_permission_required),
|
||||||
|
ToastType.Warning,
|
||||||
)
|
)
|
||||||
return@rememberLauncherForActivityResult
|
return@rememberLauncherForActivityResult
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -42,6 +42,6 @@ private fun TransferMetric(icon: ImageVector, text: String, style: TextStyle, co
|
|||||||
modifier = Modifier.size(12.dp),
|
modifier = Modifier.size(12.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(text = text.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.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.produceState
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -38,8 +37,6 @@ import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.state.TunnelsUiState
|
import com.zaneschepke.wireguardautotunnel.ui.state.TunnelsUiState
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -52,14 +49,6 @@ fun TunnelList(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val isTv = LocalIsAndroidTV.current
|
val isTv = LocalIsAndroidTV.current
|
||||||
|
|
||||||
val now by
|
|
||||||
produceState(System.currentTimeMillis()) {
|
|
||||||
while (true) {
|
|
||||||
delay(1_000L.milliseconds)
|
|
||||||
value = System.currentTimeMillis()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
@@ -105,10 +94,7 @@ fun TunnelList(
|
|||||||
uiState.backendStatus.activeTunnels[tunnel.id] ?: ActiveTunnel()
|
uiState.backendStatus.activeTunnels[tunnel.id] ?: ActiveTunnel()
|
||||||
}
|
}
|
||||||
|
|
||||||
val displayState =
|
val displayState = remember(activeTunnel) { DisplayTunnelState.from(activeTunnel) }
|
||||||
remember(activeTunnel, now, uiState.displayStates[tunnel.id]) {
|
|
||||||
uiState.displayStates[tunnel.id] ?: DisplayTunnelState.from(activeTunnel, now)
|
|
||||||
}
|
|
||||||
|
|
||||||
val isRunning = uiState.backendStatus.activeTunnels.containsKey(tunnel.id)
|
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.FontStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.dokar.sonner.ToastType
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
|
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.config.components.QrCodeDialog
|
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.config.components.QrCodeDialog
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
|
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.ConfigHeaderColor
|
import com.zaneschepke.wireguardautotunnel.ui.theme.ConfigHeaderColor
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.ConfigKeyColor
|
import com.zaneschepke.wireguardautotunnel.ui.theme.ConfigKeyColor
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isTextTooLargeForQr
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isTextTooLargeForQr
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
|
||||||
import org.koin.compose.viewmodel.koinActivityViewModel
|
import org.koin.compose.viewmodel.koinActivityViewModel
|
||||||
@@ -76,7 +78,12 @@ fun ConfigScreen(
|
|||||||
when (sideEffect) {
|
when (sideEffect) {
|
||||||
is LocalSideEffect.Modal.QR -> {
|
is LocalSideEffect.Modal.QR -> {
|
||||||
if (tunnel.quickConfig.isTextTooLargeForQr()) {
|
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 {
|
} else {
|
||||||
showQrModal = true
|
showQrModal = true
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-63
@@ -26,97 +26,57 @@ sealed class DisplayTunnelState {
|
|||||||
|
|
||||||
data object Connected : DisplayTunnelState()
|
data object Connected : DisplayTunnelState()
|
||||||
|
|
||||||
data object Degraded : DisplayTunnelState()
|
data object HandshakeFailure : DisplayTunnelState()
|
||||||
|
|
||||||
@StringRes
|
@StringRes
|
||||||
fun labelRes(): Int {
|
fun labelRes(): Int =
|
||||||
return when (this) {
|
when (this) {
|
||||||
Disconnected -> R.string.tunnel_state_disconnected
|
Disconnected -> R.string.tunnel_state_disconnected
|
||||||
Connecting -> R.string.tunnel_state_starting
|
Connecting,
|
||||||
ResolvingDns -> R.string.tunnel_state_resolving_dns
|
|
||||||
EstablishingConnection -> R.string.tunnel_state_establishing_connection
|
EstablishingConnection -> R.string.tunnel_state_establishing_connection
|
||||||
|
ResolvingDns -> R.string.tunnel_state_resolving_dns
|
||||||
Ready -> R.string.ready
|
Ready -> R.string.ready
|
||||||
Connected -> R.string.tunnel_state_connected
|
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 {
|
fun asLocalizedString(context: Context): String {
|
||||||
return context.getString(labelRes())
|
return context.getString(labelRes())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun asColor(): Color {
|
|
||||||
return when (this) {
|
|
||||||
Disconnected -> CoolGray
|
|
||||||
|
|
||||||
Connecting,
|
|
||||||
ResolvingDns,
|
|
||||||
EstablishingConnection,
|
|
||||||
Ready -> Straw
|
|
||||||
|
|
||||||
Connected -> SilverTree
|
|
||||||
|
|
||||||
Degraded -> AlertRed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val HANDSHAKE_FAILURE_DEGRADED_THRESHOLD_MS = 6_000L
|
fun from(activeTunnel: ActiveTunnel): DisplayTunnelState {
|
||||||
|
|
||||||
// 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 {
|
|
||||||
val transport = activeTunnel.transportState
|
val transport = activeTunnel.transportState
|
||||||
val bootstrap = activeTunnel.bootstrapState
|
val bootstrap = activeTunnel.bootstrapState
|
||||||
val mode = activeTunnel.mode
|
val mode = activeTunnel.mode
|
||||||
val isVpnStyle = mode is BackendMode.Vpn || mode is BackendMode.Proxy.KillSwitchPrimary
|
val isVpnStyle = mode is BackendMode.Vpn || mode is BackendMode.Proxy.KillSwitchPrimary
|
||||||
|
|
||||||
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 {
|
return when {
|
||||||
transport is Tunnel.State.Down -> Disconnected
|
transport is Tunnel.State.Down -> Disconnected
|
||||||
|
bootstrap is BootstrapState.Failed -> HandshakeFailure
|
||||||
bootstrap is BootstrapState.Failed -> Degraded
|
|
||||||
|
|
||||||
bootstrap is BootstrapState.ResolvingDns ||
|
bootstrap is BootstrapState.ResolvingDns ||
|
||||||
bootstrap is BootstrapState.UpdatingPeers -> ResolvingDns
|
bootstrap is BootstrapState.UpdatingPeers -> ResolvingDns
|
||||||
|
|
||||||
transport is Tunnel.State.Up.Healthy -> Connected
|
transport is Tunnel.State.Up.Healthy -> Connected
|
||||||
|
|
||||||
transport is Tunnel.State.Up.HandshakeFailure -> {
|
transport is Tunnel.State.Up.HandshakeFailure -> HandshakeFailure
|
||||||
val age = now - activeTunnel.lastStateChangeMs
|
|
||||||
|
|
||||||
if (recentlyResolvedPeers && bootstrapPhaseDone) {
|
transport is Tunnel.State.Starting ->
|
||||||
if (isVpnStyle) EstablishingConnection else Ready
|
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 -> {
|
else -> if (isVpnStyle) EstablishingConnection else Ready
|
||||||
when {
|
|
||||||
bootstrapPhaseDone -> if (isVpnStyle) EstablishingConnection else Ready
|
|
||||||
else -> Connecting
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bootstrapPhaseDone -> if (isVpnStyle) EstablishingConnection else Ready
|
|
||||||
else -> Connecting
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,5 +18,6 @@ data class GlobalAppUiState(
|
|||||||
val selectedTunnelCount: Int = 0,
|
val selectedTunnelCount: Int = 0,
|
||||||
val alreadyDonated: Boolean = false,
|
val alreadyDonated: Boolean = false,
|
||||||
val isPinVerified: Boolean = false,
|
val isPinVerified: Boolean = false,
|
||||||
|
val pendingWgImportUrl: String? = null,
|
||||||
val isScreenRecordingProtectionEnabled: Boolean = false,
|
val isScreenRecordingProtectionEnabled: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,14 +16,14 @@ val ElectricTeal = Color(0xFF4DD0E1)
|
|||||||
// Status colors
|
// Status colors
|
||||||
val SilverTree = Color(0xFF6DB58B)
|
val SilverTree = Color(0xFF6DB58B)
|
||||||
val AlertRed = Color(0xFFCF6679)
|
val AlertRed = Color(0xFFCF6679)
|
||||||
|
|
||||||
val Straw = Color(0xFFD4C483)
|
val Straw = Color(0xFFD4C483)
|
||||||
|
|
||||||
val Disabled = CoolGray.copy(alpha = 0.4f)
|
val Disabled = CoolGray.copy(alpha = 0.4f)
|
||||||
|
|
||||||
// Config colors
|
// Other colors
|
||||||
val ConfigHeaderColor = Color(0xFFBB86FC)
|
val ConfigHeaderColor = Color(0xFFBB86FC)
|
||||||
val ConfigKeyColor = Color(0xFF03DAC5)
|
val ConfigKeyColor = Color(0xFF03DAC5)
|
||||||
|
val Heart = Color(0xFFDB61A2)
|
||||||
|
|
||||||
sealed class ThemeColors(
|
sealed class ThemeColors(
|
||||||
val background: Color,
|
val background: Color,
|
||||||
|
|||||||
@@ -6,15 +6,11 @@ object Constants {
|
|||||||
const val BASE_LOG_FILE_NAME = "wg_tunnel_logs"
|
const val BASE_LOG_FILE_NAME = "wg_tunnel_logs"
|
||||||
|
|
||||||
const val VPN_SETTINGS_PACKAGE = "android.net.vpn.SETTINGS"
|
const val VPN_SETTINGS_PACKAGE = "android.net.vpn.SETTINGS"
|
||||||
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1 shl 10
|
|
||||||
const val SPECIAL_USE_SERVICE_TYPE_ID = 1 shl 30
|
const val SPECIAL_USE_SERVICE_TYPE_ID = 1 shl 30
|
||||||
|
|
||||||
const val QR_CODE_NAME_PROPERTY = "# Name ="
|
|
||||||
|
|
||||||
const val FDROID_FLAVOR = "fdroid"
|
const val FDROID_FLAVOR = "fdroid"
|
||||||
const val GOOGLE_PLAY_FLAVOR = "google"
|
const val GOOGLE_PLAY_FLAVOR = "google"
|
||||||
const val STANDALONE_FLAVOR = "standalone"
|
const val STANDALONE_FLAVOR = "standalone"
|
||||||
const val RELEASE = "release"
|
|
||||||
|
|
||||||
const val BASE_RELEASE_URL = "https://github.com/wgtunnel/wgtunnel/releases/tag/"
|
const val BASE_RELEASE_URL = "https://github.com/wgtunnel/wgtunnel/releases/tag/"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ object DnsValidator {
|
|||||||
return Result.Valid
|
return Result.Valid
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validateUdp(value: String): DnsValidator.Result {
|
private fun validateUdp(value: String): Result {
|
||||||
val parts = value.split(":")
|
val parts = value.split(":")
|
||||||
|
|
||||||
val host = parts.getOrNull(0)?.trim()
|
val host = parts.getOrNull(0)?.trim()
|
||||||
@@ -93,14 +93,14 @@ object DnsValidator {
|
|||||||
|
|
||||||
// basic IP/hostname sanity check
|
// basic IP/hostname sanity check
|
||||||
if (!isValidHostOrIp(host)) {
|
if (!isValidHostOrIp(host)) {
|
||||||
return DnsValidator.Result.Invalid(DnsError.InvalidIpOrHost)
|
return Result.Invalid(DnsError.InvalidIpOrHost)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (port !in 1..65535) {
|
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 {
|
private fun isValidHostOrIp(value: String): Boolean {
|
||||||
|
|||||||
@@ -1,22 +1,9 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.util
|
package com.zaneschepke.wireguardautotunnel.util
|
||||||
|
|
||||||
import com.vdurmont.semver4j.Semver
|
import com.vdurmont.semver4j.Semver
|
||||||
import java.math.BigDecimal
|
|
||||||
import kotlin.math.pow
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
object NumberUtils {
|
object NumberUtils {
|
||||||
private const val BYTES_IN_KB = 1024.0
|
|
||||||
private val BYTES_IN_MB = BYTES_IN_KB.pow(2.0)
|
|
||||||
private val keyValidationRegex = """^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=${'$'}""".toRegex()
|
|
||||||
|
|
||||||
fun bytesToMB(bytes: Long): BigDecimal {
|
|
||||||
return bytes.toBigDecimal().divide(BYTES_IN_MB.toBigDecimal())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isValidKey(key: String): Boolean {
|
|
||||||
return key.matches(keyValidationRegex)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generateRandomTunnelName(): String {
|
fun generateRandomTunnelName(): String {
|
||||||
return "tunnel${randomFive()}"
|
return "tunnel${randomFive()}"
|
||||||
|
|||||||
+14
-108
@@ -1,27 +1,17 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.util.extensions
|
package com.zaneschepke.wireguardautotunnel.util.extensions
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Context.POWER_SERVICE
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.content.pm.PackageInfo
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.PowerManager
|
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.service.quicksettings.TileService
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
import com.zaneschepke.wireguardautotunnel.MainActivity
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelControlTile
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelControlTile
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.state.TunnelApp
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -30,22 +20,11 @@ import java.util.Locale
|
|||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
fun Context.openWebUrl(url: String): Result<Unit> {
|
fun Context.openWebUrl(url: String): Result<Unit> = runCatching {
|
||||||
return kotlin
|
val webpage: Uri = url.toUri()
|
||||||
.runCatching {
|
val intent =
|
||||||
val webpage: Uri = url.toUri()
|
Intent(Intent.ACTION_VIEW, webpage).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
|
||||||
val intent =
|
startActivity(intent)
|
||||||
Intent(Intent.ACTION_VIEW, webpage).apply {
|
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
}
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
.onFailure { showToast(R.string.no_browser_detected) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.isBatteryOptimizationsDisabled(): Boolean {
|
|
||||||
val pm = getSystemService(POWER_SERVICE) as PowerManager
|
|
||||||
return pm.isIgnoringBatteryOptimizations(packageName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.launchNotificationSettings() {
|
fun Context.launchNotificationSettings() {
|
||||||
@@ -94,30 +73,7 @@ fun Context.hasSAFSupport(mimeType: String): Boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.launchShareFile(file: File) {
|
fun Context.launchSupportEmail(): Result<Unit> = runCatching {
|
||||||
FileProvider.getUriForFile(this, getString(R.string.provider), file)
|
|
||||||
val shareIntent =
|
|
||||||
Intent().apply {
|
|
||||||
action = Intent.ACTION_SEND
|
|
||||||
type = FileUtils.ALL_FILE_TYPES
|
|
||||||
putExtra(Intent.EXTRA_STREAM, file)
|
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
val chooserIntent =
|
|
||||||
Intent.createChooser(shareIntent, "").apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
|
|
||||||
this.startActivity(chooserIntent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.showToast(resId: Int) {
|
|
||||||
Toast.makeText(this, this.getString(resId), Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.showToast(message: String) {
|
|
||||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.launchSupportEmail() {
|
|
||||||
val intent =
|
val intent =
|
||||||
Intent(Intent.ACTION_SENDTO).apply {
|
Intent(Intent.ACTION_SENDTO).apply {
|
||||||
data = "mailto:".toUri()
|
data = "mailto:".toUri()
|
||||||
@@ -132,7 +88,7 @@ fun Context.launchSupportEmail() {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} 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> {
|
fun Context.launchVpnSettings(): Result<Unit> {
|
||||||
return kotlin.runCatching {
|
return kotlin.runCatching {
|
||||||
val intent =
|
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)
|
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() {
|
fun Context.launchAppSettings() {
|
||||||
kotlin
|
kotlin
|
||||||
.runCatching {
|
.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 {
|
fun Context.canInstallPackages(): Boolean {
|
||||||
return packageManager.canRequestPackageInstalls()
|
return packageManager.canRequestPackageInstalls()
|
||||||
}
|
}
|
||||||
@@ -252,9 +158,9 @@ fun Context.installApk(apkFile: File) {
|
|||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.launchPlayStoreListing() {
|
fun Context.launchPlayStoreListing(): Result<Unit> = runCatching {
|
||||||
val intent =
|
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)
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
setPackage("com.android.vending")
|
setPackage("com.android.vending")
|
||||||
}
|
}
|
||||||
@@ -262,12 +168,12 @@ fun Context.launchPlayStoreListing() {
|
|||||||
if (intent.resolveActivity(packageManager) != null) {
|
if (intent.resolveActivity(packageManager) != null) {
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
} else {
|
} else {
|
||||||
openWebUrl("https://play.google.com/store/apps/details?id=$packageName")
|
throw IllegalStateException("Play Store not found")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.launchPlayStoreReview() {
|
fun Context.launchPlayStoreReview(): Result<Unit> = runCatching {
|
||||||
val uri = Uri.parse("market://details?id=$packageName&showAllReviews=true")
|
val uri = "market://details?id=$packageName&showAllReviews=true".toUri()
|
||||||
val intent =
|
val intent =
|
||||||
Intent(Intent.ACTION_VIEW, uri).apply {
|
Intent(Intent.ACTION_VIEW, uri).apply {
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
@@ -276,7 +182,7 @@ fun Context.launchPlayStoreReview() {
|
|||||||
if (intent.resolveActivity(packageManager) != null) {
|
if (intent.resolveActivity(packageManager) != null) {
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
} else {
|
} 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) }
|
return zipWithNext().all { (a, b) -> selector(a) <= selector(b) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Int.toMillis(): Long {
|
|
||||||
return this * 1_000L
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Double.round(decimals: Int): Double {
|
fun Double.round(decimals: Int): Double {
|
||||||
val factor = 10.0.pow(decimals)
|
val factor = 10.0.pow(decimals)
|
||||||
return (this * factor).roundToInt() / factor
|
return (this * factor).roundToInt() / factor
|
||||||
|
|||||||
+13
@@ -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 {
|
fun ActiveTunnel.statusText(context: Context): String {
|
||||||
return context.getString(
|
return context.getString(
|
||||||
R.string.status_template,
|
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.core.content.PermissionChecker.PERMISSION_GRANTED
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.dokar.sonner.ToastType
|
||||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||||
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
||||||
import com.zaneschepke.tunnel.backend.RootShell
|
import com.zaneschepke.tunnel.util.RootShell
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.WifiDetectionMethod
|
import com.zaneschepke.wireguardautotunnel.domain.enums.WifiDetectionMethod
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||||
@@ -16,6 +15,8 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsR
|
|||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.AutoTunnelUiState
|
import com.zaneschepke.wireguardautotunnel.ui.state.AutoTunnelUiState
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
@@ -99,7 +100,10 @@ class AutoTunnelViewModel(
|
|||||||
val trimmed = name.trim()
|
val trimmed = name.trim()
|
||||||
if (state.autoTunnelSettings.trustedNetworkSSIDs.contains(name)) {
|
if (state.autoTunnelSettings.trustedNetworkSSIDs.contains(name)) {
|
||||||
return@intent postSideEffect(
|
return@intent postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.error_ssid_exists))
|
GlobalSideEffect.Snackbar(
|
||||||
|
StringValue.StringResource(R.string.error_ssid_exists),
|
||||||
|
ToastType.Error,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
setTrustedNetworkNames(
|
setTrustedNetworkNames(
|
||||||
@@ -141,6 +145,10 @@ class AutoTunnelViewModel(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setDisabledOnCaptivePortal(enabled: Boolean) = intent {
|
||||||
|
autoTunnelRepository.updateDisableOnCaptivePortal(enabled)
|
||||||
|
}
|
||||||
|
|
||||||
fun removeTunnelNetwork(tunnel: TunnelConfig, ssid: String) = intent {
|
fun removeTunnelNetwork(tunnel: TunnelConfig, ssid: String) = intent {
|
||||||
tunnelsRepository.save(
|
tunnelsRepository.save(
|
||||||
tunnel.copy(
|
tunnel.copy(
|
||||||
@@ -153,11 +161,19 @@ class AutoTunnelViewModel(
|
|||||||
when (method) {
|
when (method) {
|
||||||
WifiDetectionMethod.ROOT -> {
|
WifiDetectionMethod.ROOT -> {
|
||||||
val accepted = RootShell.requestRootPermission()
|
val accepted = RootShell.requestRootPermission()
|
||||||
val message =
|
if (!accepted)
|
||||||
if (!accepted) StringValue.StringResource(R.string.error_root_denied)
|
return@intent postSideEffect(
|
||||||
else StringValue.StringResource(R.string.root_accepted)
|
GlobalSideEffect.Snackbar(
|
||||||
postSideEffect(GlobalSideEffect.Snackbar(message))
|
StringValue.StringResource(R.string.error_root_denied),
|
||||||
if (!accepted) return@intent
|
ToastType.Error,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
postSideEffect(
|
||||||
|
GlobalSideEffect.Snackbar(
|
||||||
|
StringValue.StringResource(R.string.root_accepted),
|
||||||
|
ToastType.Success,
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
WifiDetectionMethod.SHIZUKU -> {
|
WifiDetectionMethod.SHIZUKU -> {
|
||||||
requestShizuku()
|
requestShizuku()
|
||||||
@@ -188,7 +204,10 @@ class AutoTunnelViewModel(
|
|||||||
)
|
)
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
postSideEffect(
|
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
|
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.dokar.sonner.ToastType
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.MimicMode
|
import com.zaneschepke.wireguardautotunnel.domain.enums.MimicMode
|
||||||
@@ -105,7 +106,10 @@ class ConfigEditViewModel(
|
|||||||
if (state.isTunnelNameTaken) {
|
if (state.isTunnelNameTaken) {
|
||||||
|
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.tunnel_name_taken))
|
GlobalSideEffect.Snackbar(
|
||||||
|
StringValue.StringResource(R.string.tunnel_name_taken),
|
||||||
|
ToastType.Error,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return@intent
|
return@intent
|
||||||
@@ -113,7 +117,10 @@ class ConfigEditViewModel(
|
|||||||
|
|
||||||
if (state.draft.tunnelName.isBlank()) {
|
if (state.draft.tunnelName.isBlank()) {
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.name_error_empty))
|
GlobalSideEffect.Snackbar(
|
||||||
|
StringValue.StringResource(R.string.name_error_empty),
|
||||||
|
ToastType.Error,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return@intent
|
return@intent
|
||||||
}
|
}
|
||||||
@@ -147,8 +154,9 @@ class ConfigEditViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Toast(
|
GlobalSideEffect.Snackbar(
|
||||||
StringValue.StringResource(R.string.config_changes_saved)
|
StringValue.StringResource(R.string.config_changes_saved),
|
||||||
|
ToastType.Success,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -164,7 +172,7 @@ class ConfigEditViewModel(
|
|||||||
else -> StringValue.StringResource(R.string.unknown_error)
|
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
|
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.dokar.sonner.ToastType
|
||||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.DnsSettingsCoordinator
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.DnsSettingsCoordinator
|
||||||
@@ -9,7 +10,6 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsReposito
|
|||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarType
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.DnsUiState
|
import com.zaneschepke.wireguardautotunnel.ui.state.DnsUiState
|
||||||
import com.zaneschepke.wireguardautotunnel.util.DnsValidator
|
import com.zaneschepke.wireguardautotunnel.util.DnsValidator
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
@@ -70,7 +70,7 @@ class DnsViewModel(
|
|||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(
|
GlobalSideEffect.Snackbar(
|
||||||
StringValue.StringResource(result.error.labelRes()),
|
StringValue.StringResource(result.error.labelRes()),
|
||||||
type = SnackbarType.WARNING,
|
type = ToastType.Error,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return@intent
|
return@intent
|
||||||
@@ -87,7 +87,10 @@ class DnsViewModel(
|
|||||||
|
|
||||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||||
postSideEffect(
|
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
|
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.dokar.sonner.ToastType
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||||
@@ -52,7 +53,10 @@ class LockdownViewModel(
|
|||||||
|
|
||||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||||
postSideEffect(
|
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 android.net.Uri
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.dokar.sonner.ToastType
|
||||||
import com.zaneschepke.logcatter.LogReader
|
import com.zaneschepke.logcatter.LogReader
|
||||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
@@ -61,7 +62,10 @@ class LoggerViewModel(
|
|||||||
fun exportLogs(uri: Uri?) = intent {
|
fun exportLogs(uri: Uri?) = intent {
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Toast(StringValue.StringResource(R.string.export_unsupported))
|
GlobalSideEffect.Snackbar(
|
||||||
|
StringValue.StringResource(R.string.export_unsupported),
|
||||||
|
ToastType.Warning,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return@intent
|
return@intent
|
||||||
}
|
}
|
||||||
@@ -76,11 +80,12 @@ class LoggerViewModel(
|
|||||||
Timber.e(action)
|
Timber.e(action)
|
||||||
intent {
|
intent {
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Toast(
|
GlobalSideEffect.Snackbar(
|
||||||
StringValue.StringResource(
|
StringValue.StringResource(
|
||||||
R.string.export_failed,
|
R.string.export_failed,
|
||||||
": ${action.localizedMessage}",
|
": ${action.localizedMessage}",
|
||||||
)
|
),
|
||||||
|
ToastType.Error,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-3
@@ -1,6 +1,7 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.dokar.sonner.ToastType
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||||
@@ -79,7 +80,8 @@ class ProxySettingsViewModel(
|
|||||||
if (socksPort == null || httpPort == null || socksPort == httpPort) {
|
if (socksPort == null || httpPort == null || socksPort == httpPort) {
|
||||||
return@intent postSideEffect(
|
return@intent postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(
|
GlobalSideEffect.Snackbar(
|
||||||
StringValue.StringResource(R.string.ports_must_differ)
|
StringValue.StringResource(R.string.ports_must_differ),
|
||||||
|
ToastType.Error,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -96,7 +98,10 @@ class ProxySettingsViewModel(
|
|||||||
|
|
||||||
if (updated.proxyPassword?.any { it.isWhitespace() } == true) {
|
if (updated.proxyPassword?.any { it.isWhitespace() } == true) {
|
||||||
postSideEffect(
|
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) }
|
return@intent reduce { state.copy(isPasswordError = true) }
|
||||||
}
|
}
|
||||||
@@ -104,7 +109,10 @@ class ProxySettingsViewModel(
|
|||||||
proxySettingsRepository.upsert(updated)
|
proxySettingsRepository.upsert(updated)
|
||||||
|
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.config_changes_saved))
|
GlobalSideEffect.Snackbar(
|
||||||
|
StringValue.StringResource(R.string.config_changes_saved),
|
||||||
|
ToastType.Success,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-6
@@ -1,7 +1,8 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||||
|
|
||||||
import androidx.lifecycle.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.R
|
||||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
||||||
@@ -96,11 +97,19 @@ class SettingsViewModel(
|
|||||||
fun setTunnelScriptedEnabled(to: Boolean) = intent {
|
fun setTunnelScriptedEnabled(to: Boolean) = intent {
|
||||||
if (to) {
|
if (to) {
|
||||||
val accepted = RootShell.requestRootPermission()
|
val accepted = RootShell.requestRootPermission()
|
||||||
val message =
|
if (!accepted)
|
||||||
if (!accepted) StringValue.StringResource(R.string.error_root_denied)
|
return@intent postSideEffect(
|
||||||
else StringValue.StringResource(R.string.root_accepted)
|
GlobalSideEffect.Snackbar(
|
||||||
postSideEffect(GlobalSideEffect.Snackbar(message))
|
StringValue.StringResource(R.string.error_root_denied),
|
||||||
if (!accepted) return@intent
|
ToastType.Error,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
postSideEffect(
|
||||||
|
GlobalSideEffect.Snackbar(
|
||||||
|
StringValue.StringResource(R.string.root_accepted),
|
||||||
|
ToastType.Success,
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
settingsRepository.upsert(state.settings.copy(tunnelScriptingEnabled = to))
|
settingsRepository.upsert(state.settings.copy(tunnelScriptingEnabled = to))
|
||||||
}
|
}
|
||||||
|
|||||||
+62
-30
@@ -3,11 +3,10 @@ package com.zaneschepke.wireguardautotunnel.viewmodel
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.dokar.sonner.ToastType
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelModeCoordinator
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelModeCoordinator
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
||||||
@@ -17,6 +16,8 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.SelectedTunnelsRepo
|
|||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||||
import com.zaneschepke.wireguardautotunnel.parser.ConfigParseException
|
import com.zaneschepke.wireguardautotunnel.parser.ConfigParseException
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
|
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.GlobalAppUiState
|
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.QuickConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelName
|
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelName
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asStringValue
|
import com.zaneschepke.wireguardautotunnel.util.extensions.asStringValue
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isHtmlResponse
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.saveTunnelsUniquely
|
import com.zaneschepke.wireguardautotunnel.util.extensions.saveTunnelsUniquely
|
||||||
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
|
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
@@ -77,7 +79,7 @@ class SharedAppViewModel(
|
|||||||
|
|
||||||
val displayStates =
|
val displayStates =
|
||||||
backendStatus.activeTunnels.mapValues { (_, activeTunnel) ->
|
backendStatus.activeTunnels.mapValues { (_, activeTunnel) ->
|
||||||
DisplayTunnelState.from(activeTunnel, System.currentTimeMillis())
|
DisplayTunnelState.from(activeTunnel)
|
||||||
}
|
}
|
||||||
|
|
||||||
TunnelsUiState(
|
TunnelsUiState(
|
||||||
@@ -177,6 +179,10 @@ class SharedAppViewModel(
|
|||||||
appStateRepository.setShouldShowDonationSnackbar(to)
|
appStateRepository.setShouldShowDonationSnackbar(to)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showSnackMessage(message: StringValue, type: ToastType) = intent {
|
||||||
|
postGlobalSideEffect(GlobalSideEffect.Snackbar(message, type))
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun postSideEffect(globalSideEffect: GlobalSideEffect) {
|
suspend fun postSideEffect(globalSideEffect: GlobalSideEffect) {
|
||||||
globalEffectRepository.post(globalSideEffect)
|
globalEffectRepository.post(globalSideEffect)
|
||||||
}
|
}
|
||||||
@@ -187,12 +193,6 @@ class SharedAppViewModel(
|
|||||||
globalEffectRepository.post(sideEffect)
|
globalEffectRepository.post(sideEffect)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showSnackMessage(message: StringValue) = intent {
|
|
||||||
postGlobalSideEffect(GlobalSideEffect.Snackbar(message))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showToast(message: StringValue) = intent { postSideEffect(GlobalSideEffect.Toast(message)) }
|
|
||||||
|
|
||||||
fun disableBatteryOptimizationsShown() = intent {
|
fun disableBatteryOptimizationsShown() = intent {
|
||||||
appStateRepository.setBatteryOptimizationDisableShown(true)
|
appStateRepository.setBatteryOptimizationDisableShown(true)
|
||||||
}
|
}
|
||||||
@@ -200,14 +200,20 @@ class SharedAppViewModel(
|
|||||||
fun saveSortChanges(tunnels: List<TunnelConfig>) = intent {
|
fun saveSortChanges(tunnels: List<TunnelConfig>) = intent {
|
||||||
tunnelRepository.saveAll(tunnels.mapIndexed { index, conf -> conf.copy(position = index) })
|
tunnelRepository.saveAll(tunnels.mapIndexed { index, conf -> conf.copy(position = index) })
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.config_changes_saved))
|
GlobalSideEffect.Snackbar(
|
||||||
|
StringValue.StringResource(R.string.config_changes_saved),
|
||||||
|
ToastType.Success,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sortByLatency(tunnels: List<TunnelConfig>) = intent {
|
fun sortByLatency(tunnels: List<TunnelConfig>) = intent {
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.pinging_servers))
|
GlobalSideEffect.Snackbar(
|
||||||
|
StringValue.StringResource(R.string.pinging_servers),
|
||||||
|
ToastType.Info,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
val sortedResult =
|
val sortedResult =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
@@ -249,12 +255,17 @@ class SharedAppViewModel(
|
|||||||
TunnelConfig.tunnelConfFromQuick(config, name)
|
TunnelConfig.tunnelConfFromQuick(config, name)
|
||||||
}
|
}
|
||||||
tunnelRepository.saveTunnelsUniquely(tunnelConfigs, state.tunnelNames.map { it.value })
|
tunnelRepository.saveTunnelsUniquely(tunnelConfigs, state.tunnelNames.map { it.value })
|
||||||
} catch (_: IOException) {
|
} catch (e: Exception) {
|
||||||
postSideEffect(
|
if (e is ConfigParseException) {
|
||||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.read_failed))
|
postSideEffect(GlobalSideEffect.Snackbar(e.asStringValue(), ToastType.Error))
|
||||||
)
|
} else {
|
||||||
} catch (e: ConfigParseException) {
|
postSideEffect(
|
||||||
postSideEffect(GlobalSideEffect.Snackbar(e.asStringValue()))
|
GlobalSideEffect.Snackbar(
|
||||||
|
StringValue.StringResource(R.string.config_error),
|
||||||
|
ToastType.Error,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,22 +275,38 @@ class SharedAppViewModel(
|
|||||||
|
|
||||||
fun importFromQr(conf: String) = intent { importFromClipboard(conf) }
|
fun importFromQr(conf: String) = intent { importFromClipboard(conf) }
|
||||||
|
|
||||||
|
fun promptWgImport(url: String) = intent { reduce { state.copy(pendingWgImportUrl = url) } }
|
||||||
|
|
||||||
|
fun dismissWgImport() = intent { reduce { state.copy(pendingWgImportUrl = null) } }
|
||||||
|
|
||||||
fun importFromUrl(url: String) = intent {
|
fun importFromUrl(url: String) = intent {
|
||||||
|
reduce { state.copy(pendingWgImportUrl = null) }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
httpClient.prepareGet(url).execute { response ->
|
httpClient.prepareGet(url).execute { response ->
|
||||||
if (response.status.value in 200..299) {
|
if (response.status.value !in 200..299) {
|
||||||
val body = response.bodyAsText()
|
throw IOException("Server returned error: ${response.status.value}")
|
||||||
importFromClipboard(body)
|
|
||||||
} else {
|
|
||||||
throw IOException(
|
|
||||||
"Failed to download file with error status: ${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) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
postSideEffect(
|
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)
|
is IOException -> StringValue.StringResource(R.string.error_download_failed)
|
||||||
else -> StringValue.StringResource(R.string.error_file_extension)
|
else -> StringValue.StringResource(R.string.error_file_extension)
|
||||||
}
|
}
|
||||||
postSideEffect(GlobalSideEffect.Toast(message))
|
postSideEffect(GlobalSideEffect.Snackbar(message, ToastType.Error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,7 +353,8 @@ class SharedAppViewModel(
|
|||||||
if (selectedTuns.any { activeTunIds?.contains(it.id) == true })
|
if (selectedTuns.any { activeTunIds?.contains(it.id) == true })
|
||||||
return@intent postSideEffect(
|
return@intent postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(
|
GlobalSideEffect.Snackbar(
|
||||||
StringValue.StringResource(R.string.delete_active_message)
|
StringValue.StringResource(R.string.delete_active_message),
|
||||||
|
ToastType.Error,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
tunnelRepository.delete(selectedTuns)
|
tunnelRepository.delete(selectedTuns)
|
||||||
@@ -349,11 +377,12 @@ class SharedAppViewModel(
|
|||||||
val onFailure = { action: Throwable ->
|
val onFailure = { action: Throwable ->
|
||||||
intent {
|
intent {
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Toast(
|
GlobalSideEffect.Snackbar(
|
||||||
StringValue.StringResource(
|
StringValue.StringResource(
|
||||||
R.string.export_failed,
|
R.string.export_failed,
|
||||||
": ${action.localizedMessage}",
|
": ${action.localizedMessage}",
|
||||||
)
|
),
|
||||||
|
ToastType.Error,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -370,7 +399,10 @@ class SharedAppViewModel(
|
|||||||
if (it.exists()) it.delete()
|
if (it.exists()) it.delete()
|
||||||
}
|
}
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.export_success))
|
GlobalSideEffect.Snackbar(
|
||||||
|
StringValue.StringResource(R.string.export_success),
|
||||||
|
ToastType.Success,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
clearSelectedTunnels()
|
clearSelectedTunnels()
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -1,6 +1,7 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.dokar.sonner.ToastType
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.InstalledPackageRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.InstalledPackageRepository
|
||||||
@@ -88,9 +89,14 @@ class SplitTunnelViewModel(
|
|||||||
editableInterface.copy(includedApplications = included, excludedApplications = excluded)
|
editableInterface.copy(includedApplications = included, excludedApplications = excluded)
|
||||||
val updatedProxyConfig = editableConfig.copy(`interface` = updatedInterface)
|
val updatedProxyConfig = editableConfig.copy(`interface` = updatedInterface)
|
||||||
val updatedConfig = updatedProxyConfig.buildConfig()
|
val updatedConfig = updatedProxyConfig.buildConfig()
|
||||||
tunnelRepository.save(tunnel.copy(quickConfig = updatedConfig.asQuickString()))
|
tunnelRepository.save(
|
||||||
|
tunnel.copy(quickConfig = updatedConfig.withName(tunnel.name).asQuickString())
|
||||||
|
)
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.config_changes_saved))
|
GlobalSideEffect.Snackbar(
|
||||||
|
StringValue.StringResource(R.string.config_changes_saved),
|
||||||
|
ToastType.Success,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-6
@@ -1,6 +1,7 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.dokar.sonner.ToastType
|
||||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.AppUpdate
|
import com.zaneschepke.wireguardautotunnel.domain.model.AppUpdate
|
||||||
@@ -25,7 +26,10 @@ class SupportViewModel(
|
|||||||
|
|
||||||
fun checkForStandaloneUpdate() = intent {
|
fun checkForStandaloneUpdate() = intent {
|
||||||
postSideEffect(
|
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) }
|
reduce { state.copy(isLoading = true) }
|
||||||
updateRepository
|
updateRepository
|
||||||
@@ -33,15 +37,19 @@ class SupportViewModel(
|
|||||||
.onSuccess { update ->
|
.onSuccess { update ->
|
||||||
if (update == null) {
|
if (update == null) {
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Toast(
|
GlobalSideEffect.Snackbar(
|
||||||
StringValue.StringResource(R.string.latest_installed)
|
StringValue.StringResource(R.string.latest_installed),
|
||||||
|
ToastType.Info,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else reduce { state.copy(appUpdate = update.sanitized()) }
|
} else reduce { state.copy(appUpdate = update.sanitized()) }
|
||||||
}
|
}
|
||||||
.onFailure {
|
.onFailure {
|
||||||
postSideEffect(
|
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) }
|
reduce { state.copy(isLoading = false) }
|
||||||
@@ -83,8 +91,9 @@ class SupportViewModel(
|
|||||||
.onSuccess { postSideEffect(GlobalSideEffect.InstallApk(it)) }
|
.onSuccess { postSideEffect(GlobalSideEffect.InstallApk(it)) }
|
||||||
.onFailure {
|
.onFailure {
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Toast(
|
GlobalSideEffect.Snackbar(
|
||||||
StringValue.StringResource(R.string.update_download_failed)
|
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="wifi_settings">Wi-Fi settings</string>
|
||||||
<string name="tunnel_on_wifi">Tunnel on Wi-Fi</string>
|
<string name="tunnel_on_wifi">Tunnel on Wi-Fi</string>
|
||||||
<string name="add_peer">Add peer</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="persistent_keepalive">Persistent keepalive</string>
|
||||||
<string name="info">Info</string>
|
<string name="info">Info</string>
|
||||||
<string name="exclude">Exclude</string>
|
<string name="exclude">Exclude</string>
|
||||||
|
|||||||
@@ -245,7 +245,7 @@
|
|||||||
<string name="root_required_template">%1$s (vyžaduje root)</string>
|
<string name="root_required_template">%1$s (vyžaduje root)</string>
|
||||||
<string name="recommended_template">%1$s (doporučeno)</string>
|
<string name="recommended_template">%1$s (doporučeno)</string>
|
||||||
<string name="hint_template">(%1$s)</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="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="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>
|
<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="wifi_settings">Wi-Fi settings</string>
|
||||||
<string name="tunnel_on_wifi">Tunnel on Wi-Fi</string>
|
<string name="tunnel_on_wifi">Tunnel on Wi-Fi</string>
|
||||||
<string name="add_peer">Add peer</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="persistent_keepalive">Persistent keepalive</string>
|
||||||
<string name="info">Info</string>
|
<string name="info">Info</string>
|
||||||
<string name="exclude">Exclude</string>
|
<string name="exclude">Exclude</string>
|
||||||
|
|||||||
@@ -138,6 +138,7 @@
|
|||||||
<string name="config_error">Ungültige Konfiguration</string>
|
<string name="config_error">Ungültige Konfiguration</string>
|
||||||
<string name="join_matrix">Matrix-Community beitreten</string>
|
<string name="join_matrix">Matrix-Community beitreten</string>
|
||||||
<string name="error_download_failed">Download der Konfiguration fehlgeschlagen</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="add_from_url">Von URL hinzufügen</string>
|
||||||
<string name="export_logs">Gespeicherte Logs exportieren</string>
|
<string name="export_logs">Gespeicherte Logs exportieren</string>
|
||||||
<string name="app_permission_title">Steuere Tunnel und Auto-Tunnel Funktionen.</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_running">Auto-Tunnel läuft</string>
|
||||||
<string name="auto_tunnel_not_running">Auto-Tunnel läuft nicht</string>
|
<string name="auto_tunnel_not_running">Auto-Tunnel läuft nicht</string>
|
||||||
<string name="tunnel_monitoring">Tunnelüberwachung</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="restore_success">Wiederherstellerfolg. %1$s</string>
|
||||||
<string name="restarting_app">Starte App neu, um Änderungen anzuwenden …</string>
|
<string name="restarting_app">Starte App neu, um Änderungen anzuwenden …</string>
|
||||||
<string name="restore_failed">Wiederherstellung aus Backup fehlgeschlagen.</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_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="auto_tunnel_not_running">El túnel automático no está activo</string>
|
||||||
<string name="tunnel_monitoring">Monitoreo del túnel</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="restore_success">Restauración completada. %1$s</string>
|
||||||
<string name="restarting_app">Reiniciando la app para aplicar los cambios…</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>
|
<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="recommended_template">%1$s (soovitatav)</string>
|
||||||
<string name="hint_template">(%1$s)</string>
|
<string name="hint_template">(%1$s)</string>
|
||||||
<string name="tunnel_monitoring">Tunnelo monitooring</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="restore_success">Taastamine õnnestus. %1$s</string>
|
||||||
<string name="restarting_app">Muudatuste jõustamiseks taaskäivitan rakenduse…</string>
|
<string name="restarting_app">Muudatuste jõustamiseks taaskäivitan rakenduse…</string>
|
||||||
<string name="restore_failed">Varukoopiast taastamine ei õnnestunud.</string>
|
<string name="restore_failed">Varukoopiast taastamine ei õnnestunud.</string>
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
<string name="wifi_settings">Wi-Fi settings</string>
|
<string name="wifi_settings">Wi-Fi settings</string>
|
||||||
<string name="tunnel_on_wifi">Tunnel on Wi-Fi</string>
|
<string name="tunnel_on_wifi">Tunnel on Wi-Fi</string>
|
||||||
<string name="add_peer">Add peer</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="persistent_keepalive">Persistent keepalive</string>
|
||||||
<string name="info">Info</string>
|
<string name="info">Info</string>
|
||||||
<string name="exclude">Exclude</string>
|
<string name="exclude">Exclude</string>
|
||||||
|
|||||||
@@ -136,7 +136,7 @@
|
|||||||
<string name="show_qr">Show QR</string>
|
<string name="show_qr">Show QR</string>
|
||||||
<string name="wifi_settings">Wi-Fi settings</string>
|
<string name="wifi_settings">Wi-Fi settings</string>
|
||||||
<string name="add_peer">Add peer</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="persistent_keepalive">Persistent keepalive</string>
|
||||||
<string name="info">Info</string>
|
<string name="info">Info</string>
|
||||||
<string name="backup_failed">Failed to create backup.</string>
|
<string name="backup_failed">Failed to create backup.</string>
|
||||||
|
|||||||
@@ -171,7 +171,7 @@
|
|||||||
<string name="mimic_quic">Imiter QUIC</string>
|
<string name="mimic_quic">Imiter QUIC</string>
|
||||||
<string name="show_qr">Afficher le QR</string>
|
<string name="show_qr">Afficher le QR</string>
|
||||||
<string name="wifi_settings">Paramètres Wi-Fi</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="info">Info</string>
|
||||||
<string name="backup_failed">Échec de la création de la sauvegarde.</string>
|
<string name="backup_failed">Échec de la création de la sauvegarde.</string>
|
||||||
<string name="location_permissions">Permissions de localisation</string>
|
<string name="location_permissions">Permissions de localisation</string>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
<string name="wifi_settings">Wi-Fi settings</string>
|
<string name="wifi_settings">Wi-Fi settings</string>
|
||||||
<string name="tunnel_on_wifi">Tunnel on Wi-Fi</string>
|
<string name="tunnel_on_wifi">Tunnel on Wi-Fi</string>
|
||||||
<string name="add_peer">Add peer</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="persistent_keepalive">Persistent keepalive</string>
|
||||||
<string name="info">Info</string>
|
<string name="info">Info</string>
|
||||||
<string name="exclude">Exclude</string>
|
<string name="exclude">Exclude</string>
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
<string name="wifi_settings">Wi-Fi beállítások</string>
|
<string name="wifi_settings">Wi-Fi beállítások</string>
|
||||||
<string name="tunnel_on_wifi">Alagút Wi-Fi-n</string>
|
<string name="tunnel_on_wifi">Alagút Wi-Fi-n</string>
|
||||||
<string name="add_peer">Peer hozzáadása</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="persistent_keepalive">Kapcsolatmegőrzés</string>
|
||||||
<string name="info">Infó</string>
|
<string name="info">Infó</string>
|
||||||
<string name="exclude">Kizárás</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="auto_tunnel_channel_description">Saluran untuk notifikasi status terowongan otomatis</string>
|
||||||
<string name="show_qr">Tampilkan QR</string>
|
<string name="show_qr">Tampilkan QR</string>
|
||||||
<string name="wifi_settings">Pengaturan Wi-Fi</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="info">Info</string>
|
||||||
<string name="exclude">Kecualikan</string>
|
<string name="exclude">Kecualikan</string>
|
||||||
<string name="backup_failed">Gagal membuat cadangan.</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