mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 186f063e23 | |||
| 2c78e9fcbd | |||
| 8bdeff515e | |||
| fa89715ede | |||
| 4a94905893 | |||
| cf184f2042 | |||
| e0ddb8730d | |||
| c8c041b872 | |||
| 7c8adb380b | |||
| 614f97fd14 | |||
| fbd470f5d2 | |||
| 5f89b2ed31 | |||
| 9503a3284b | |||
| 68c1a19bd3 | |||
| f3bb6667c3 | |||
| 244a990c37 | |||
| cbf07600b4 | |||
| ec8d90d13d | |||
| 85acca8604 | |||
| 0a9773d202 | |||
| 3cb4480a65 | |||
| a7f3255a76 | |||
| 7d7b99f448 | |||
| 74e9e462bb | |||
| 619e3c1cde | |||
| 77f8a8215b | |||
| 8772036dd7 | |||
| 63625ccbd7 | |||
| 9ac7ae77b3 | |||
| e062fbb34d | |||
| 16d5586433 | |||
| 48a3ad64f4 | |||
| e5796d641d | |||
| daf5eebdd2 | |||
| 4c725491f4 | |||
| 7529c11172 | |||
| 83f530df42 | |||
| 8083ab9526 | |||
| 7d1312da0f | |||
| d4dbc43c70 | |||
| 294f2624c7 | |||
| 0603cb2fdd | |||
| 48ddbcbb0e | |||
| e6c3e3f5b3 | |||
| 0d75699b40 | |||
| 5c98aab9e0 | |||
| a1e3489ba2 | |||
| bcd19b5494 | |||
| 160a6ca84d | |||
| aaf7ebd326 | |||
| b8c75a45e4 | |||
| ac17a09e19 | |||
| c51a7ee393 | |||
| c534516e33 | |||
| 9c999cc62c | |||
| cc3c865211 | |||
| 8648a67fdc | |||
| 9ee1fa69ed | |||
| 379ffdcbbf | |||
| 6e3c1324b2 | |||
| 660bea0104 | |||
| 2b8610fa8a | |||
| 944034ac74 | |||
| 9f394aeffb |
@@ -70,16 +70,16 @@ jobs:
|
||||
outputs:
|
||||
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up JDK 17
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
java-version: '21'
|
||||
cache: gradle
|
||||
|
||||
- name: Grant execute permission for gradlew
|
||||
|
||||
@@ -72,15 +72,15 @@ jobs:
|
||||
outputs:
|
||||
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
- name: Set up JDK 17
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
java-version: '21'
|
||||
cache: gradle
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
@@ -112,6 +112,9 @@ jobs:
|
||||
./gradlew :app:assemble${flavor^}Debug --stacktrace
|
||||
;;
|
||||
esac
|
||||
env:
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
GITHUB_RUN_NUMBER: ${{ github.run_number }}
|
||||
- name: Get release apk path
|
||||
id: apk-path
|
||||
run: echo "path=$(find . -regex '^.*/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
has_new_commits: ${{ steps.check.outputs.new_commits }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
- name: Check for new commits
|
||||
id: check
|
||||
env:
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
name: on-pr
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
format_check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up JDK 17
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Verify Gradle Wrapper
|
||||
uses: gradle/actions/wrapper-validation@v6
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
java-version: '21'
|
||||
cache: gradle
|
||||
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Run ktfmt
|
||||
run: ./gradlew ktfmtCheck
|
||||
run: ./gradlew ktfmtCheck
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
name: publish-github
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event_name == 'push' && github.ref || 'master' }}
|
||||
- name: Install system dependencies
|
||||
@@ -205,7 +205,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-google-aab
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event_name == 'push' && github.ref || 'master' }}
|
||||
|
||||
|
||||
+53
-47
@@ -1,6 +1,5 @@
|
||||
import com.android.build.api.dsl.ApplicationExtension
|
||||
import com.android.build.api.variant.FilterConfiguration
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
@@ -9,6 +8,7 @@ plugins {
|
||||
alias(libs.plugins.compose.compiler)
|
||||
alias(libs.plugins.grgit)
|
||||
alias(libs.plugins.licensee)
|
||||
alias(libs.plugins.aboutlibraries)
|
||||
}
|
||||
|
||||
ksp {
|
||||
@@ -18,15 +18,14 @@ ksp {
|
||||
licensee {
|
||||
allowedLicenses().forEach { allow(it) }
|
||||
allowedLicenseUrls().forEach { allowUrl(it) }
|
||||
// foss, but missing licenses
|
||||
ignoreDependencies("com.github.T8RIN.QuickieExtended")
|
||||
ignoreDependencies("com.github.topjohnwu.libsu")
|
||||
}
|
||||
allowDependency("com.github.T8RIN.QuickieExtended", "quickie-foss", "1.18.1") {
|
||||
because("FOSS library, but JitPack doesn't publish license metadata")
|
||||
allow("Apache-2.0")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_17
|
||||
freeCompilerArgs = listOf("-XXLanguage:+PropertyParamAnnotationDefaultTargetMode")
|
||||
allowDependency("com.github.topjohnwu.libsu", "core", "6.0.0") {
|
||||
because("FOSS library, but JitPack doesn't publish license metadata")
|
||||
allow("Apache-2.0")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,10 +45,11 @@ configure<ApplicationExtension> {
|
||||
|
||||
splits {
|
||||
abi {
|
||||
isEnable = !project.hasProperty("noSplits")
|
||||
val noSplits = providers.gradleProperty("noSplits").isPresent
|
||||
isEnable = !noSplits
|
||||
reset()
|
||||
include("armeabi-v7a", "arm64-v8a")
|
||||
isUniversalApk = !project.hasProperty("noSplits")
|
||||
isUniversalApk = !noSplits
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,14 +57,17 @@ configure<ApplicationExtension> {
|
||||
applicationId = Constants.APP_ID
|
||||
minSdk = Constants.MIN_SDK
|
||||
targetSdk = Constants.TARGET_SDK
|
||||
versionCode = computeVersionCode()
|
||||
versionName = computeVersionName()
|
||||
versionCode = Constants.VERSION_CODE
|
||||
versionName = Constants.VERSION_NAME
|
||||
|
||||
experimentalProperties["android.experimental.disableGitVersion"] = true
|
||||
|
||||
sourceSets {
|
||||
getByName("debug").assets.directories += "$projectDir/schemas"
|
||||
}
|
||||
|
||||
val languagesArray = buildLanguagesArray(languageList())
|
||||
val languagesProvider = project.languageListProvider()
|
||||
val languagesArray = buildLanguagesArray(languagesProvider.get())
|
||||
buildConfigField("String[]", "LANGUAGES", "new String[]{ $languagesArray }")
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
@@ -98,21 +101,24 @@ configure<ApplicationExtension> {
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
signingConfig = signingConfigs.getByName(Constants.RELEASE)
|
||||
resValue("string", "provider", "\"${Constants.APP_NAME}.provider\"")
|
||||
manifestPlaceholders["providerAuthority"] = "${Constants.APP_NAME}.provider"
|
||||
buildConfigField("String", "FILE_PROVIDER_AUTHORITY", "\"${Constants.APP_NAME}.provider\"")
|
||||
}
|
||||
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
resValue("string", "app_name", "WG Tunnel Debug")
|
||||
isDebuggable = true
|
||||
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.debug\"")
|
||||
manifestPlaceholders["providerAuthority"] = "${Constants.APP_NAME}.provider.debug"
|
||||
buildConfigField("String", "FILE_PROVIDER_AUTHORITY", "\"${Constants.APP_NAME}.provider.debug\"")
|
||||
}
|
||||
|
||||
create(Constants.NIGHTLY) {
|
||||
initWith(buildTypes.getByName(Constants.RELEASE))
|
||||
applicationIdSuffix = ".nightly"
|
||||
resValue("string", "app_name", "WG Tunnel Nightly")
|
||||
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.nightly\"")
|
||||
manifestPlaceholders["providerAuthority"] = "${Constants.APP_NAME}.provider.nightly"
|
||||
buildConfigField("String", "FILE_PROVIDER_AUTHORITY", "\"${Constants.APP_NAME}.provider.nightly\"")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,8 +140,6 @@ configure<ApplicationExtension> {
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
@@ -148,31 +152,42 @@ configure<ApplicationExtension> {
|
||||
|
||||
androidComponents {
|
||||
onVariants { variant ->
|
||||
val isNightly = project.isNightlyBuild()
|
||||
|
||||
val abiNameMap =
|
||||
mapOf(
|
||||
"armeabi-v7a" to "armv7",
|
||||
"arm64-v8a" to "arm64",
|
||||
"x86" to "x86",
|
||||
"x86_64" to "x64",
|
||||
)
|
||||
if (isNightly) {
|
||||
variant.outputs.forEach { output ->
|
||||
|
||||
output.versionCode.set(
|
||||
output.versionCode.get() + project.getVersionCodeIncrement()
|
||||
)
|
||||
|
||||
val currentVersion = output.versionName.get()
|
||||
val nextVersion = bumpToNextPatchVersion(currentVersion)
|
||||
val gitHash = project.getGitCommitHash()
|
||||
|
||||
output.versionName.set("$nextVersion-nightly+git.$gitHash")
|
||||
}
|
||||
}
|
||||
|
||||
val abiNameMap = mapOf(
|
||||
"armeabi-v7a" to "armv7",
|
||||
"arm64-v8a" to "arm64",
|
||||
"x86" to "x86",
|
||||
"x86_64" to "x64",
|
||||
)
|
||||
|
||||
variant.outputs.forEach { output ->
|
||||
val abi = output.filters.find { it.filterType == FilterConfiguration.FilterType.ABI }?.identifier
|
||||
|
||||
val flavorName = variant.productFlavors.joinToString("-") { it.second }
|
||||
|
||||
val versionName = output.versionName.get()
|
||||
|
||||
val baseFileName = "${Constants.APP_NAME}-${flavorName}-v${versionName}"
|
||||
|
||||
val outputFileName =
|
||||
if (!abi.isNullOrEmpty()) {
|
||||
val shortAbiName = abiNameMap.getOrDefault(abi, abi)
|
||||
"${baseFileName}-${shortAbiName}.apk"
|
||||
} else {
|
||||
"${baseFileName}.apk"
|
||||
}
|
||||
val outputFileName = if (!abi.isNullOrEmpty()) {
|
||||
val shortAbiName = abiNameMap.getOrDefault(abi, abi)
|
||||
"${baseFileName}-${shortAbiName}.apk"
|
||||
} else {
|
||||
"${baseFileName}.apk"
|
||||
}
|
||||
|
||||
output.outputFileName.set(outputFileName)
|
||||
}
|
||||
@@ -225,6 +240,8 @@ dependencies {
|
||||
// UI utilities
|
||||
implementation(libs.bundles.ui.utilities)
|
||||
implementation(libs.lottie.compose)
|
||||
implementation(libs.sonner)
|
||||
implementation(libs.aboutlibraries.compose)
|
||||
|
||||
// Misc utilities
|
||||
implementation(libs.bundles.misc.utilities)
|
||||
@@ -265,17 +282,6 @@ dependencies {
|
||||
implementation(libs.koin.worker)
|
||||
}
|
||||
|
||||
tasks.register<Copy>("copyLicenseeJsonToAssets") {
|
||||
dependsOn("licensee")
|
||||
val outputAssets = layout.projectDirectory.dir("src/main/assets")
|
||||
from(layout.buildDirectory.file("reports/licensee/androidFdroidRelease/artifacts.json")) {
|
||||
rename("artifacts.json", "licenses.json")
|
||||
}
|
||||
into(outputAssets)
|
||||
}
|
||||
|
||||
tasks.named("preBuild") { dependsOn("copyLicenseeJsonToAssets") }
|
||||
|
||||
// https://gist.github.com/obfusk/61046e09cee352ae6dd109911534b12e#fix-proposed-by-linsui-disable-baseline-profiles
|
||||
tasks.configureEach {
|
||||
if (name.contains("ArtProfile")) {
|
||||
|
||||
@@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,520 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 32,
|
||||
"identityHash": "fd4803fc483f41704303be9246dcfb4d",
|
||||
"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, `seamless_roaming_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"
|
||||
},
|
||||
{
|
||||
"fieldPath": "seamlessRoamingEnabled",
|
||||
"columnName": "seamless_roaming_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, 'fd4803fc483f41704303be9246dcfb4d')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,6 @@
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.App.Start"
|
||||
tools:targetApi="tiramisu">
|
||||
@@ -74,6 +73,13 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="wg" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<action android:name="android.intent.action.SHOW_APP_INFO" />
|
||||
@@ -150,7 +156,7 @@
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="@string/provider"
|
||||
android:authorities="${providerAuthority}"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
@@ -169,9 +175,9 @@
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
<service
|
||||
android:name=".core.service.tile.TunnelControlTile"
|
||||
android:name=".service.tile.TunnelControlTile"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_notification"
|
||||
android:icon="@drawable/qs_logo"
|
||||
android:label="@string/tunnel_control"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<meta-data
|
||||
@@ -186,9 +192,9 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".core.service.tile.AutoTunnelControlTile"
|
||||
android:name=".service.tile.AutoTunnelControlTile"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_notification"
|
||||
android:icon="@drawable/qs_logo"
|
||||
android:label="@string/auto_tunnel"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<meta-data
|
||||
@@ -203,7 +209,7 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".core.service.autotunnel.AutoTunnelService"
|
||||
android:name=".service.autotunnel.AutoTunnelService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse"
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
import ProxySettingsScreen
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
@@ -19,17 +21,29 @@ import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.CheckCircleOutline
|
||||
import androidx.compose.material.icons.outlined.ErrorOutline
|
||||
import androidx.compose.material.icons.outlined.FavoriteBorder
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material.icons.outlined.WarningAmber
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -38,20 +52,19 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||
@@ -59,6 +72,10 @@ import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import com.dokar.sonner.TextToastAction
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.dokar.sonner.Toaster
|
||||
import com.dokar.sonner.rememberToasterState
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
@@ -69,11 +86,9 @@ import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.banner.AppAlertBanner
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.LocalNetworkPermissionDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarInfo
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarType
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.rememberCustomSnackbarState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.SecureRoute
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Tab
|
||||
@@ -110,19 +125,29 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.ipv6.IPv6
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.sort.SortScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.SplitTunnelScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.AlertRed
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Heart
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.OffWhite
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Straw
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.installApk
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.restartApp
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
||||
import com.zaneschepke.wireguardautotunnel.util.permission.LocalNetworkPermissionHelper
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigEditViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
|
||||
import de.raphaelebner.roomdatabasebackup.core.OnCompleteListener.Companion.EXIT_CODE_ERROR
|
||||
import de.raphaelebner.roomdatabasebackup.core.OnCompleteListener.Companion.EXIT_CODE_ERROR_DECRYPTION_ERROR
|
||||
import de.raphaelebner.roomdatabasebackup.core.OnCompleteListener.Companion.EXIT_CODE_ERROR_RESTORE_BACKUP_IS_ENCRYPTED
|
||||
import de.raphaelebner.roomdatabasebackup.core.OnCompleteListener.Companion.EXIT_CODE_ERROR_WRONG_DECRYPTION_PASSWORD
|
||||
import de.raphaelebner.roomdatabasebackup.core.RoomBackup
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
@@ -132,6 +157,7 @@ import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.orbitmvi.orbit.compose.collectAsState
|
||||
import timber.log.Timber
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
@@ -155,9 +181,10 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
handleIncomingIntent(intent)
|
||||
roomBackup = RoomBackup(this).database(appDatabase).enableLogDebug(true).maxFileCount(5)
|
||||
|
||||
roomBackup = RoomBackup(this)
|
||||
handleConfigFileIntent(intent)
|
||||
handleWgDeepLinkIntent(intent)
|
||||
|
||||
installSplashScreen().apply {
|
||||
setKeepOnScreenCondition { !viewModel.container.stateFlow.value.isAppLoaded }
|
||||
@@ -175,12 +202,54 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
val snackbarState = rememberCustomSnackbarState()
|
||||
val toaster = rememberToasterState()
|
||||
var showVpnPermissionDialog by remember { mutableStateOf(false) }
|
||||
var vpnPermissionDenied by remember { mutableStateOf(false) }
|
||||
var requestingTunnelMode by remember {
|
||||
mutableStateOf<Pair<TunnelMode?, TunnelConfig?>>(Pair(null, null))
|
||||
}
|
||||
var showLocalNetworkRationale by remember { mutableStateOf(false) }
|
||||
var hasPromptedLocalNetwork by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val localNetworkPermissionLauncher =
|
||||
rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.RequestPermission()
|
||||
) { isGranted ->
|
||||
if (!isGranted) {
|
||||
val canAskAgain =
|
||||
ActivityCompat.shouldShowRequestPermissionRationale(
|
||||
this,
|
||||
Manifest.permission.ACCESS_LOCAL_NETWORK,
|
||||
)
|
||||
|
||||
if (!canAskAgain) {
|
||||
val intent =
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = Uri.fromParts("package", packageName, null)
|
||||
}
|
||||
startActivity(intent)
|
||||
} else {
|
||||
toaster.show(
|
||||
message =
|
||||
context.getString(R.string.local_network_permission_denied),
|
||||
type = ToastType.Warning,
|
||||
duration = 6000.milliseconds,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(uiState.isAppLoaded) {
|
||||
if (
|
||||
uiState.isAppLoaded &&
|
||||
!hasPromptedLocalNetwork &&
|
||||
LocalNetworkPermissionHelper.shouldRequestPermission() &&
|
||||
!LocalNetworkPermissionHelper.isPermissionGranted(context)
|
||||
) {
|
||||
hasPromptedLocalNetwork = true
|
||||
showLocalNetworkRationale = true
|
||||
}
|
||||
}
|
||||
|
||||
val startingStack = buildList {
|
||||
add(Route.Tunnels)
|
||||
@@ -232,22 +301,18 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
is GlobalSideEffect.Snackbar -> {
|
||||
scope.launch {
|
||||
snackbarState.showSnackbar(
|
||||
SnackbarInfo(
|
||||
message =
|
||||
buildAnnotatedString {
|
||||
append(sideEffect.message.asString(context))
|
||||
},
|
||||
type = sideEffect.type ?: SnackbarType.INFO,
|
||||
durationMs = sideEffect.durationMs ?: 4000L,
|
||||
)
|
||||
)
|
||||
when (sideEffect.type) {
|
||||
ToastType.Warning,
|
||||
ToastType.Error -> toaster.dismissAll()
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
is GlobalSideEffect.Toast ->
|
||||
scope.launch { context.showToast(sideEffect.message.asString(context)) }
|
||||
toaster.show(
|
||||
message = sideEffect.message.asString(context),
|
||||
type = sideEffect.type,
|
||||
duration = (sideEffect.durationMs ?: 4000L).milliseconds,
|
||||
)
|
||||
}
|
||||
|
||||
is GlobalSideEffect.LaunchUrl -> context.openWebUrl(sideEffect.url)
|
||||
is GlobalSideEffect.InstallApk -> context.installApk(sideEffect.apk)
|
||||
@@ -275,49 +340,58 @@ class MainActivity : AppCompatActivity() {
|
||||
},
|
||||
)
|
||||
|
||||
val annotatedMessage = buildAnnotatedString {
|
||||
append(context.getString(R.string.donation_prompt_prefix))
|
||||
append(" ")
|
||||
withLink(
|
||||
LinkAnnotation.Clickable(
|
||||
tag = context.getString(R.string.support),
|
||||
styles =
|
||||
TextLinkStyles(
|
||||
style =
|
||||
SpanStyle(
|
||||
textDecoration = TextDecoration.Underline,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
focusedStyle =
|
||||
SpanStyle(
|
||||
textDecoration = TextDecoration.Underline,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
background =
|
||||
MaterialTheme.colorScheme.primary.copy(
|
||||
alpha = 0.2f
|
||||
),
|
||||
),
|
||||
),
|
||||
) {
|
||||
snackbarState.dismissCurrent()
|
||||
navController.push(Route.Donate)
|
||||
}
|
||||
) {
|
||||
append(context.getString(R.string.donation_prompt_link))
|
||||
}
|
||||
append(" ")
|
||||
append(context.getString(R.string.donation_prompt_suffix))
|
||||
if (showLocalNetworkRationale) {
|
||||
LocalNetworkPermissionDialog(
|
||||
onDismiss = {
|
||||
showLocalNetworkRationale = false
|
||||
toaster.show(
|
||||
message =
|
||||
context.getString(R.string.local_network_permission_denied),
|
||||
type = ToastType.Warning,
|
||||
duration = 6000.milliseconds,
|
||||
)
|
||||
},
|
||||
onAttest = {
|
||||
showLocalNetworkRationale = false
|
||||
|
||||
localNetworkPermissionLauncher.launch(
|
||||
Manifest.permission.ACCESS_LOCAL_NETWORK
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
uiState.pendingWgImportUrl?.let { url ->
|
||||
val host = Uri.parse(url).host ?: url
|
||||
InfoDialog(
|
||||
onDismiss = { viewModel.dismissWgImport() },
|
||||
onAttest = { viewModel.importFromUrl(url) },
|
||||
title = stringResource(R.string.add_from_url),
|
||||
body = { Text(stringResource(R.string.wg_url_confirm_message, host)) },
|
||||
confirmText = stringResource(R.string.okay),
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (uiState.shouldShowDonationSnackbar && !uiState.alreadyDonated) {
|
||||
viewModel.setShouldShowDonationSnackbar(false)
|
||||
snackbarState.showSnackbar(
|
||||
SnackbarInfo(
|
||||
message = annotatedMessage,
|
||||
type = SnackbarType.THANK_YOU,
|
||||
durationMs = 30_000L,
|
||||
)
|
||||
toaster.show(
|
||||
message =
|
||||
context.getString(R.string.donation_prompt_prefix) +
|
||||
" " +
|
||||
context.getString(R.string.donation_prompt_link) +
|
||||
" " +
|
||||
context.getString(R.string.donation_prompt_suffix),
|
||||
type = ToastType.Normal,
|
||||
duration = 30_000L.milliseconds,
|
||||
action =
|
||||
TextToastAction(
|
||||
text = context.getString(R.string.donate_title),
|
||||
onClick = { toastId ->
|
||||
toaster.dismiss(toastId)
|
||||
navController.push(Route.Donate)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -378,25 +452,6 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
Scaffold(
|
||||
snackbarHost = {
|
||||
snackbarState.SnackbarHost(
|
||||
modifier =
|
||||
Modifier.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 80.dp)
|
||||
) { info ->
|
||||
CustomSnackBar(
|
||||
message = info.message,
|
||||
type = info.type,
|
||||
onDismiss = { snackbarState.dismissCurrent() },
|
||||
containerColor =
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||
2.dp
|
||||
),
|
||||
modifier =
|
||||
Modifier.wrapContentHeight(align = Alignment.Top),
|
||||
)
|
||||
}
|
||||
},
|
||||
topBar = { DynamicTopAppBar(navState) },
|
||||
bottomBar = {
|
||||
if (navState.showBottomItems) {
|
||||
@@ -548,6 +603,70 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
}
|
||||
Toaster(
|
||||
state = toaster,
|
||||
alignment = Alignment.BottomCenter,
|
||||
offset = IntOffset(0, -220),
|
||||
richColors = true,
|
||||
background = {
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp),
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp),
|
||||
)
|
||||
)
|
||||
},
|
||||
elevation = 1.dp,
|
||||
shadowAmbientColor =
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f),
|
||||
shadowSpotColor =
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||
border = {
|
||||
BorderStroke(
|
||||
0.dp,
|
||||
androidx.compose.ui.graphics.Color.Transparent,
|
||||
)
|
||||
},
|
||||
actionSlot = { toast ->
|
||||
(toast.action as? TextToastAction)?.let { action ->
|
||||
TextButton(
|
||||
onClick = { action.onClick(toast) },
|
||||
colors =
|
||||
ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.primary
|
||||
),
|
||||
contentPadding = PaddingValues(horizontal = 12.dp),
|
||||
) {
|
||||
Text(text = action.text, fontWeight = FontWeight.Medium)
|
||||
}
|
||||
}
|
||||
},
|
||||
iconSlot = { toast ->
|
||||
val (icon, color) =
|
||||
when (toast.type) {
|
||||
ToastType.Success ->
|
||||
Icons.Outlined.CheckCircleOutline to SilverTree
|
||||
ToastType.Error ->
|
||||
Icons.Outlined.ErrorOutline to AlertRed
|
||||
ToastType.Warning ->
|
||||
Icons.Outlined.WarningAmber to Straw
|
||||
ToastType.Info ->
|
||||
Icons.Outlined.Info to
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
ToastType.Normal ->
|
||||
Icons.Outlined.FavoriteBorder to Heart
|
||||
}
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = color,
|
||||
modifier = Modifier.padding(end = 12.dp),
|
||||
)
|
||||
},
|
||||
contentColor = { MaterialTheme.colorScheme.onSurface },
|
||||
shape = { RoundedCornerShape(16.dp) },
|
||||
showCloseButton = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -555,55 +674,14 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
fun performBackup() = lifecycleScope.launch {
|
||||
roomBackup
|
||||
.database(appDatabase)
|
||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||
.enableLogDebug(true)
|
||||
.maxFileCount(5)
|
||||
.apply {
|
||||
onCompleteListener { success, _, _ ->
|
||||
lifecycleScope.launch {
|
||||
if (success) {
|
||||
showToast(
|
||||
getString(
|
||||
R.string.backup_success,
|
||||
getString(R.string.restarting_app),
|
||||
)
|
||||
)
|
||||
restartApp()
|
||||
} else {
|
||||
showToast(R.string.backup_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun handleWgDeepLinkIntent(intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_VIEW) {
|
||||
val uri = intent.data ?: return
|
||||
if (uri.scheme == "wg") {
|
||||
val httpsUrl = uri.toString().replaceFirst("wg://", "https://")
|
||||
viewModel.promptWgImport(httpsUrl)
|
||||
}
|
||||
.backup()
|
||||
}
|
||||
|
||||
fun performRestore() = lifecycleScope.launch {
|
||||
roomBackup
|
||||
.database(appDatabase)
|
||||
.enableLogDebug(true)
|
||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||
.apply {
|
||||
onCompleteListener { success, _, _ ->
|
||||
lifecycleScope.launch {
|
||||
if (success) {
|
||||
showToast(
|
||||
getString(
|
||||
R.string.restore_success,
|
||||
getString(R.string.restarting_app),
|
||||
)
|
||||
)
|
||||
restartApp()
|
||||
} else {
|
||||
showToast(R.string.restore_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.restore()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -611,21 +689,105 @@ class MainActivity : AppCompatActivity() {
|
||||
networkMonitor.checkPermissionsAndUpdateState()
|
||||
}
|
||||
|
||||
fun performBackup(encrypt: Boolean = false, password: String? = null) {
|
||||
roomBackup
|
||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||
.apply {
|
||||
if (encrypt && !password.isNullOrBlank()) {
|
||||
backupIsEncrypted(true)
|
||||
customEncryptPassword(password)
|
||||
}
|
||||
}
|
||||
.onCompleteListener { success, _, _ ->
|
||||
lifecycleScope.launch {
|
||||
val sideEffect =
|
||||
if (success) {
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.backup_success),
|
||||
ToastType.Success,
|
||||
)
|
||||
} else {
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.backup_failed),
|
||||
ToastType.Error,
|
||||
)
|
||||
}
|
||||
viewModel.postSideEffect(sideEffect)
|
||||
}
|
||||
}
|
||||
.backup()
|
||||
}
|
||||
|
||||
fun performRestore(encrypt: Boolean = false, password: String? = null) {
|
||||
roomBackup
|
||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||
.apply {
|
||||
if (encrypt && !password.isNullOrBlank()) {
|
||||
backupIsEncrypted(true)
|
||||
customEncryptPassword(password)
|
||||
}
|
||||
}
|
||||
.onCompleteListener { success, message, exitCode ->
|
||||
lifecycleScope.launch {
|
||||
if (success) {
|
||||
viewModel.postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.restore_success),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
roomBackup.restartApp(Intent(this@MainActivity, MainActivity::class.java))
|
||||
} else {
|
||||
Timber.w("Restore failed, exitCode=$exitCode, message=$message")
|
||||
|
||||
val errorMessage =
|
||||
when (exitCode) {
|
||||
EXIT_CODE_ERROR_WRONG_DECRYPTION_PASSWORD ->
|
||||
getString(R.string.restore_failed_wrong_password)
|
||||
|
||||
EXIT_CODE_ERROR,
|
||||
EXIT_CODE_ERROR_DECRYPTION_ERROR,
|
||||
EXIT_CODE_ERROR_RESTORE_BACKUP_IS_ENCRYPTED ->
|
||||
getString(R.string.restore_failed_invalid_file)
|
||||
|
||||
else -> getString(R.string.restore_failed)
|
||||
}
|
||||
|
||||
viewModel.postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.DynamicString(errorMessage),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.restore()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
handleIncomingIntent(intent)
|
||||
handleConfigFileIntent(intent)
|
||||
handleWgDeepLinkIntent(intent)
|
||||
}
|
||||
|
||||
private fun handleIncomingIntent(intent: Intent?) {
|
||||
private fun handleConfigFileIntent(intent: Intent?) {
|
||||
intent ?: return
|
||||
|
||||
when (intent.action) {
|
||||
Intent.ACTION_VIEW,
|
||||
Intent.ACTION_EDIT,
|
||||
Intent.ACTION_SEND -> {
|
||||
val uri: Uri? = intent.data
|
||||
uri?.let { viewModel.importFromUri(it) }
|
||||
val uri: Uri? = intent.data ?: return
|
||||
val name = uri?.lastPathSegment?.lowercase() ?: return
|
||||
if (
|
||||
!name.endsWith(FileUtils.CONF_FILE_EXTENSION) &&
|
||||
!name.endsWith(FileUtils.ZIP_FILE_EXTENSION)
|
||||
) {
|
||||
Timber.d("Ignoring non-config URI in handleIncomingIntent: $uri")
|
||||
return
|
||||
}
|
||||
viewModel.importFromUri(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,8 @@ import com.zaneschepke.tunnel.backend.Backend
|
||||
import com.zaneschepke.tunnel.di.tunnelModule
|
||||
import com.zaneschepke.tunnel.service.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.core.event.TunnelEventDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AppBoostrapCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||
import com.zaneschepke.wireguardautotunnel.di.Dispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.di.Scope
|
||||
@@ -21,6 +18,9 @@ import com.zaneschepke.wireguardautotunnel.di.dispatchersModule
|
||||
import com.zaneschepke.wireguardautotunnel.di.networkModule
|
||||
import com.zaneschepke.wireguardautotunnel.di.tunnelBackendProviderModule
|
||||
import com.zaneschepke.wireguardautotunnel.di.workerModule
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -51,6 +51,13 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
||||
|
||||
private val backend: Backend by inject()
|
||||
|
||||
private val alwaysOnCallback =
|
||||
object : VpnService.AlwaysOnCallback {
|
||||
override fun alwaysOnTriggered() {
|
||||
applicationScope.launch { tunnelCoordinator.startDefault() }
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(KoinViewModelScopeApi::class)
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
@@ -71,11 +78,10 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
||||
lazyModules(networkModule)
|
||||
}
|
||||
instance = this
|
||||
|
||||
notificationService.createAllChannels()
|
||||
|
||||
// Sync tiles
|
||||
AutoTunnelTileRefresher.refresh(this)
|
||||
TunnelTileRefresher.refresh(this)
|
||||
syncTiles()
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
@@ -87,13 +93,7 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
||||
Timber.plant(ReleaseTree())
|
||||
}
|
||||
|
||||
backend.setAlwaysOnCallback(
|
||||
object : VpnService.AlwaysOnCallback {
|
||||
override fun alwaysOnTriggered() {
|
||||
applicationScope.launch { tunnelCoordinator.startDefault() }
|
||||
}
|
||||
}
|
||||
)
|
||||
backend.setAlwaysOnCallback(alwaysOnCallback)
|
||||
|
||||
val dispatcher = get<TunnelEventDispatcher>()
|
||||
val coordinator = get<TunnelCoordinator>()
|
||||
@@ -111,6 +111,11 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
||||
applicationScope.launch(ioDispatcher) { boostrapCoordinator.bootstrap() }
|
||||
}
|
||||
|
||||
private fun syncTiles() {
|
||||
AutoTunnelTileRefresher.refresh(this)
|
||||
TunnelTileRefresher.refresh(this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var instance: WireGuardAutoTunnel
|
||||
private set
|
||||
|
||||
+1
-1
@@ -3,11 +3,11 @@ package com.zaneschepke.wireguardautotunnel.core.broadcast
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.di.Scope
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
|
||||
+33
-29
@@ -48,47 +48,51 @@ class RemoteControlReceiver : BroadcastReceiver(), KoinComponent {
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
|
||||
val action = intent.action ?: return
|
||||
val appAction = Action.fromAction(action) ?: return
|
||||
|
||||
val pendingResult = goAsync()
|
||||
applicationScope.launch {
|
||||
val settings = settingsRepository.getGeneralSettings()
|
||||
try {
|
||||
val action = intent.action ?: return@launch
|
||||
val appAction = Action.fromAction(action) ?: return@launch
|
||||
|
||||
if (!settings.isRemoteControlEnabled) return@launch
|
||||
val settings = settingsRepository.getGeneralSettings()
|
||||
|
||||
if (!validateKey(settings, intent)) return@launch
|
||||
if (!settings.isRemoteControlEnabled) return@launch
|
||||
|
||||
when (appAction) {
|
||||
Action.START_TUNNEL -> {
|
||||
val tunnel =
|
||||
resolveTunnel(intent)
|
||||
?: tunnelsRepository.getDefaultTunnel()
|
||||
?: return@launch
|
||||
if (!validateKey(settings, intent)) return@launch
|
||||
|
||||
tunnelCoordinator.startTunnel(tunnel)
|
||||
}
|
||||
when (appAction) {
|
||||
Action.START_TUNNEL -> {
|
||||
val tunnel =
|
||||
resolveTunnel(intent)
|
||||
?: tunnelsRepository.getDefaultTunnel()
|
||||
?: return@launch
|
||||
|
||||
Action.STOP_TUNNEL -> {
|
||||
val tunnelName = intent.getStringExtra(EXTRA_TUN_NAME)
|
||||
|
||||
if (tunnelName == null) {
|
||||
tunnelCoordinator.stopActiveTunnels()
|
||||
return@launch
|
||||
tunnelCoordinator.startTunnel(tunnel)
|
||||
}
|
||||
|
||||
val tunnel = tunnelsRepository.findByTunnelName(tunnelName) ?: return@launch
|
||||
Action.STOP_TUNNEL -> {
|
||||
val tunnelName = intent.getStringExtra(EXTRA_TUN_NAME)
|
||||
|
||||
tunnelCoordinator.stopTunnel(tunnel.id)
|
||||
}
|
||||
if (tunnelName == null) {
|
||||
tunnelCoordinator.stopActiveTunnels()
|
||||
return@launch
|
||||
}
|
||||
|
||||
Action.START_AUTO_TUNNEL -> {
|
||||
autoTunnelCoordinator.enable()
|
||||
}
|
||||
val tunnel = tunnelsRepository.findByTunnelName(tunnelName) ?: return@launch
|
||||
|
||||
Action.STOP_AUTO_TUNNEL -> {
|
||||
autoTunnelCoordinator.disable()
|
||||
tunnelCoordinator.stopTunnel(tunnel.id)
|
||||
}
|
||||
|
||||
Action.START_AUTO_TUNNEL -> {
|
||||
autoTunnelCoordinator.enable()
|
||||
}
|
||||
|
||||
Action.STOP_AUTO_TUNNEL -> {
|
||||
autoTunnelCoordinator.disable()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
pendingResult.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+23
-11
@@ -27,19 +27,31 @@ class RestartReceiver : BroadcastReceiver(), KoinComponent {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
Timber.d("RestartReceiver triggered with action: ${intent.action}")
|
||||
|
||||
val pendingResult = goAsync()
|
||||
|
||||
applicationScope.launch {
|
||||
when (intent.action) {
|
||||
Intent.ACTION_BOOT_COMPLETED,
|
||||
"android.intent.action.QUICKBOOT_POWERON",
|
||||
"com.htc.intent.action.QUICKBOOT_POWERON" -> {
|
||||
startupCoordinator.applyStartupPolicy()
|
||||
}
|
||||
Intent.ACTION_MY_PACKAGE_REPLACED -> {
|
||||
Timber.i("Restoring state on package upgrade")
|
||||
startupCoordinator.applyStartupPolicy()
|
||||
logReader.deleteAndClearLogs()
|
||||
appStateRepository.setShouldShowDonationSnackbar(true)
|
||||
try {
|
||||
when (intent.action) {
|
||||
Intent.ACTION_BOOT_COMPLETED,
|
||||
"android.intent.action.QUICKBOOT_POWERON",
|
||||
"com.htc.intent.action.QUICKBOOT_POWERON" -> {
|
||||
startupCoordinator.applyStartupPolicy()
|
||||
}
|
||||
|
||||
Intent.ACTION_MY_PACKAGE_REPLACED -> {
|
||||
Timber.i("Restoring state on package upgrade")
|
||||
startupCoordinator.applyStartupPolicy()
|
||||
logReader.deleteAndClearLogs()
|
||||
appStateRepository.setShouldShowDonationSnackbar(true)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Timber.w("Unhandled action in RestartReceiver: ${intent.action}")
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
pendingResult.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+156
-18
@@ -1,14 +1,19 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.event
|
||||
|
||||
import android.content.Context
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.tunnel.event.TunnelEvent
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.tunnel.state.BackendStatus
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.TunnelNotificationLine
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.TunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.lifecyle.AppVisibilityObserver
|
||||
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationLine
|
||||
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
@@ -20,11 +25,14 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class TunnelEventDispatcher(
|
||||
private val notificationManager: TunnelNotificationService,
|
||||
private val tunnelRepository: TunnelRepository,
|
||||
private val context: Context,
|
||||
private val appVisibilityObserver: AppVisibilityObserver,
|
||||
private val globalEffectRepository: GlobalEffectRepository,
|
||||
) {
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
@@ -36,54 +44,174 @@ class TunnelEventDispatcher(
|
||||
tunnelDisplayStates: StateFlow<Map<Int, DisplayTunnelState>>,
|
||||
) {
|
||||
|
||||
// informational events
|
||||
// Informational events from tunnel backend
|
||||
providerEvents
|
||||
.distinctUntilChanged()
|
||||
.onEach { event ->
|
||||
when (event) {
|
||||
is TunnelEvent.FallbackToIpv4 -> {
|
||||
val name = getTunnelName(event.tunnelId)
|
||||
notificationManager.showIpv4Fallback(name)
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message =
|
||||
StringValue.DynamicString(
|
||||
context.getString(
|
||||
R.string.notification_ipv4_fallback_message,
|
||||
name,
|
||||
)
|
||||
),
|
||||
type = ToastType.Info,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = { notificationManager.showIpv4Fallback(name) },
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelEvent.RecoveredToIpv6 -> {
|
||||
val name = getTunnelName(event.tunnelId)
|
||||
notificationManager.showIpv6Recovery(name)
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message =
|
||||
StringValue.DynamicString(
|
||||
context.getString(
|
||||
R.string.notification_ipv6_recovery_message,
|
||||
name,
|
||||
)
|
||||
),
|
||||
type = ToastType.Success,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = { notificationManager.showIpv6Recovery(name) },
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelEvent.DynamicDnsUpdate -> {
|
||||
val name = getTunnelName(event.tunnelId)
|
||||
notificationManager.showDynamicDnsUpdate(name)
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message =
|
||||
StringValue.DynamicString(
|
||||
context.getString(
|
||||
R.string.notification_dynamic_dns_message,
|
||||
name,
|
||||
)
|
||||
),
|
||||
type = ToastType.Info,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = { notificationManager.showDynamicDnsUpdate(name) },
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelEvent.NoRootShellAccess -> {
|
||||
notificationManager.showRootShellAccess()
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message =
|
||||
StringValue.DynamicString(
|
||||
context.getString(R.string.error_root_denied)
|
||||
),
|
||||
type = ToastType.Error,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = { notificationManager.showRootShellAccess() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(scope)
|
||||
|
||||
// errors from the coordinator
|
||||
// Errors from our tunnel coordinator
|
||||
coordinatorErrors
|
||||
.distinctUntilChanged()
|
||||
.onEach { error ->
|
||||
when (error) {
|
||||
is TunnelErrorEvent.VpnPermissionDenied -> {
|
||||
notificationManager.showVpnRequired()
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message =
|
||||
StringValue.DynamicString(
|
||||
context.getString(R.string.vpn_permission_required)
|
||||
),
|
||||
type = ToastType.Error,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = { notificationManager.showVpnRequired() },
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelErrorEvent.InternalFailure -> {
|
||||
notificationManager.showError(error.message)
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message = StringValue.DynamicString(error.message),
|
||||
type = ToastType.Error,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = { notificationManager.showError(error.message) },
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelErrorEvent.Socks5PortUnavailable -> {
|
||||
val name = getTunnelName(error.tunnelId)
|
||||
notificationManager.showSocks5PortUnavailable(error.port, name)
|
||||
val message =
|
||||
context.getString(R.string.error_socks5_port_unavailable, error.port)
|
||||
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message = StringValue.DynamicString(message),
|
||||
type = ToastType.Error,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = {
|
||||
notificationManager.showSocks5PortUnavailable(error.port, name)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelErrorEvent.HttpPortUnavailable -> {
|
||||
val name = getTunnelName(error.tunnelId)
|
||||
notificationManager.showHttpPortUnavailable(error.port, name)
|
||||
val message =
|
||||
context.getString(R.string.error_http_port_unavailable, error.port)
|
||||
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message = StringValue.DynamicString(message),
|
||||
type = ToastType.Error,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = {
|
||||
notificationManager.showHttpPortUnavailable(error.port, name)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,8 +234,7 @@ class TunnelEventDispatcher(
|
||||
val tunnel = allTunnels.find { it.id == id } ?: return@mapNotNull null
|
||||
|
||||
val displayState =
|
||||
displayStates[id]
|
||||
?: DisplayTunnelState.from(activeTunnel, System.currentTimeMillis())
|
||||
displayStates[id] ?: DisplayTunnelState.from(activeTunnel)
|
||||
|
||||
TunnelNotificationLine(
|
||||
id = id,
|
||||
@@ -135,8 +262,7 @@ class TunnelEventDispatcher(
|
||||
|
||||
val tunnel = allTunnels.find { it.id == id } ?: return@mapNotNull null
|
||||
val displayState =
|
||||
displayStates[id]
|
||||
?: DisplayTunnelState.from(activeTunnel, System.currentTimeMillis())
|
||||
displayStates[id] ?: DisplayTunnelState.from(activeTunnel)
|
||||
|
||||
TunnelNotificationLine(
|
||||
id = id,
|
||||
@@ -154,6 +280,18 @@ class TunnelEventDispatcher(
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
private fun showOrNotify(
|
||||
scope: CoroutineScope,
|
||||
foregroundAction: suspend () -> Unit,
|
||||
backgroundAction: () -> Unit,
|
||||
) {
|
||||
if (appVisibilityObserver.isForeground.value) {
|
||||
scope.launch { foregroundAction() }
|
||||
} else {
|
||||
backgroundAction()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getTunnelName(tunnelId: Int): String {
|
||||
return tunnelRepository.getById(tunnelId)?.name ?: context.getString(R.string.unknown)
|
||||
}
|
||||
|
||||
+6
-2
@@ -39,7 +39,7 @@ class AppBoostrapCoordinator(
|
||||
listOf(
|
||||
async { bootstrapDns() },
|
||||
async { ensureGlobalConfig() },
|
||||
async { restoreLockdown() },
|
||||
async { restoreBackendConfiguration() },
|
||||
)
|
||||
|
||||
try {
|
||||
@@ -73,9 +73,13 @@ class AppBoostrapCoordinator(
|
||||
tunnelRepository.ensureGlobalConfigExists()
|
||||
}
|
||||
|
||||
private suspend fun restoreLockdown() {
|
||||
private suspend fun restoreBackendConfiguration() {
|
||||
val settings = settingsRepository.getGeneralSettings()
|
||||
|
||||
if (settings.seamlessRoamingEnabled) {
|
||||
tunnelProvider.setSeamlessRoaming(true)
|
||||
}
|
||||
|
||||
when (settings.tunnelMode) {
|
||||
TunnelMode.LOCK_DOWN -> {
|
||||
val lockdownSettings = lockdownRepository.getLockdownSettings()
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.orchestration
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||
|
||||
class AutoTunnelCoordinator(
|
||||
private val repository: AutoTunnelSettingsRepository,
|
||||
|
||||
+6
-2
@@ -5,7 +5,7 @@ import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
|
||||
|
||||
class TunnelModeCoordinator(
|
||||
class TunnelBackendCoordinator(
|
||||
private val tunnelProvider: TunnelProvider,
|
||||
private val settingsRepository: GeneralSettingRepository,
|
||||
private val lockdownRepository: LockdownSettingsRepository,
|
||||
@@ -42,7 +42,6 @@ class TunnelModeCoordinator(
|
||||
when (newMode) {
|
||||
TunnelMode.LOCK_DOWN -> {
|
||||
val lockdownSettings = lockdownRepository.getLockdownSettings()
|
||||
|
||||
tunnelProvider.setLockDown(lockdownSettings).getOrThrow()
|
||||
}
|
||||
|
||||
@@ -50,4 +49,9 @@ class TunnelModeCoordinator(
|
||||
TunnelMode.PROXY -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun changeSeamlessRoaming(enabled: Boolean) {
|
||||
tunnelProvider.setSeamlessRoaming(enabled).getOrThrow()
|
||||
settingsRepository.updateSeamlessRoaming(enabled)
|
||||
}
|
||||
}
|
||||
+36
-9
@@ -2,7 +2,6 @@ package com.zaneschepke.wireguardautotunnel.core.orchestration
|
||||
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.wireguardautotunnel.core.event.TunnelErrorEvent
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.RoomDnsSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelActionSource
|
||||
@@ -10,13 +9,16 @@ import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.TunnelActionEvent
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.MonitoringSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -44,6 +46,7 @@ class TunnelCoordinator(
|
||||
dnsSettingsRepository: RoomDnsSettingsRepository,
|
||||
monitoringSettingsRepository: MonitoringSettingsRepository,
|
||||
proxyRepository: ProxySettingsRepository,
|
||||
lockdownModeRepository: LockdownSettingsRepository,
|
||||
scope: CoroutineScope,
|
||||
) {
|
||||
|
||||
@@ -55,7 +58,7 @@ class TunnelCoordinator(
|
||||
tunnelProvider.backendStatus
|
||||
.map { status ->
|
||||
status.activeTunnels.mapValues { (_, activeTunnel) ->
|
||||
DisplayTunnelState.from(activeTunnel, System.currentTimeMillis())
|
||||
DisplayTunnelState.from(activeTunnel)
|
||||
}
|
||||
}
|
||||
.debounce(400L.milliseconds)
|
||||
@@ -66,6 +69,7 @@ class TunnelCoordinator(
|
||||
val dns: DnsSettings,
|
||||
val monitoring: MonitoringSettings,
|
||||
val proxy: ProxySettings,
|
||||
val lockdown: LockdownSettings,
|
||||
)
|
||||
|
||||
private val runtimeSettingsSnapshot =
|
||||
@@ -74,12 +78,14 @@ class TunnelCoordinator(
|
||||
dnsSettingsRepository.flow,
|
||||
monitoringSettingsRepository.flow,
|
||||
proxyRepository.flow,
|
||||
) { general, dns, monitoring, proxy ->
|
||||
lockdownModeRepository.flow,
|
||||
) { general, dns, monitoring, proxy, lockdown ->
|
||||
RuntimeSettingsSnapshot(
|
||||
general = general,
|
||||
dns = dns,
|
||||
monitoring = monitoring,
|
||||
proxy = proxy,
|
||||
lockdown = lockdown,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -117,7 +123,7 @@ class TunnelCoordinator(
|
||||
|
||||
// enforce single tunnel, for now
|
||||
if (backendStatus.value.activeTunnels.isNotEmpty()) {
|
||||
stopActiveTunnelsInternal()
|
||||
stopActiveTunnelsInternal(source)
|
||||
}
|
||||
|
||||
startTunnelInternal(config, source)
|
||||
@@ -131,7 +137,13 @@ class TunnelCoordinator(
|
||||
stopTunnelInternal(id, source)
|
||||
}
|
||||
|
||||
suspend fun stopActiveTunnels() = tunnelMutex.withLock { stopActiveTunnelsInternal() }
|
||||
suspend fun stopActiveTunnels(source: TunnelActionSource = TunnelActionSource.USER) =
|
||||
tunnelMutex.withLock {
|
||||
if (source == TunnelActionSource.USER) {
|
||||
_userOverrideFlow.tryEmit(Unit)
|
||||
}
|
||||
stopActiveTunnelsInternal(source)
|
||||
}
|
||||
|
||||
private suspend fun startTunnelInternal(
|
||||
tunnelConfig: TunnelConfig,
|
||||
@@ -143,6 +155,7 @@ class TunnelCoordinator(
|
||||
val dnsSettings = snapshot.dns
|
||||
val proxySettings = snapshot.proxy
|
||||
val monitoringSettings = snapshot.monitoring
|
||||
val lockdownSettings = snapshot.lockdown
|
||||
|
||||
val config = tunnelConfig.getConfig()
|
||||
val policy =
|
||||
@@ -178,8 +191,10 @@ class TunnelCoordinator(
|
||||
}
|
||||
|
||||
TunnelMode.LOCK_DOWN -> {
|
||||
|
||||
BackendMode.Proxy.KillSwitchPrimary(runConfig)
|
||||
BackendMode.Proxy.KillSwitchPrimary(
|
||||
runConfig,
|
||||
lockdownSettings.toKillSwitchConfig(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,6 +221,10 @@ class TunnelCoordinator(
|
||||
|
||||
suspend fun toggleTunnels(source: TunnelActionSource = TunnelActionSource.USER) =
|
||||
tunnelMutex.withLock {
|
||||
if (source == TunnelActionSource.USER) {
|
||||
_userOverrideFlow.tryEmit(Unit)
|
||||
}
|
||||
|
||||
val active = tunnelProvider.backendStatus.value.activeTunnels
|
||||
if (active.isNotEmpty()) {
|
||||
lastActiveTunnels = active.keys.toList()
|
||||
@@ -214,7 +233,7 @@ class TunnelCoordinator(
|
||||
_actions.emit(TunnelActionEvent.Stopped(tunnelId = id, source = source))
|
||||
}
|
||||
|
||||
stopActiveTunnelsInternal()
|
||||
stopActiveTunnelsInternal(source)
|
||||
return@withLock
|
||||
}
|
||||
|
||||
@@ -239,7 +258,15 @@ class TunnelCoordinator(
|
||||
.onFailure { _errors.emit(TunnelErrorEvent.from(it, id)) }
|
||||
}
|
||||
|
||||
private suspend fun stopActiveTunnelsInternal() {
|
||||
private suspend fun stopActiveTunnelsInternal(
|
||||
source: TunnelActionSource = TunnelActionSource.USER
|
||||
) {
|
||||
val active = tunnelProvider.backendStatus.value.activeTunnels
|
||||
|
||||
active.keys.forEach { id ->
|
||||
_actions.emit(TunnelActionEvent.Stopped(tunnelId = id, source = source))
|
||||
}
|
||||
|
||||
tunnelProvider.stopActiveTunnels()
|
||||
}
|
||||
}
|
||||
|
||||
+3
-4
@@ -19,9 +19,8 @@ class ShortcutsActivity : ComponentActivity() {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
applicationScope.launch {
|
||||
shortcutCoordinator.handle(intent)
|
||||
finish()
|
||||
}
|
||||
finish()
|
||||
|
||||
applicationScope.launch { shortcutCoordinator.handle(intent) }
|
||||
}
|
||||
}
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.tunnel.ApplicationProvider
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.tunnel.state.BackendStatus
|
||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.PROXY_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.VPN_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationLine
|
||||
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
class AndroidApplicationProvider(
|
||||
private val notificationService: NotificationService,
|
||||
private val tunnelNotificationService: TunnelNotificationService,
|
||||
private val tunnelRepository: TunnelRepository,
|
||||
) : ApplicationProvider {
|
||||
|
||||
private val context: Context = notificationService.context
|
||||
|
||||
override fun refreshTile(context: Context) {
|
||||
TunnelTileRefresher.refresh(context)
|
||||
}
|
||||
|
||||
override fun createVpnConfigurePendingIntent(context: Context): PendingIntent {
|
||||
return PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
Intent(context, MainActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
},
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
||||
)
|
||||
}
|
||||
|
||||
override val vpnInitNotification: Notification
|
||||
get() =
|
||||
notificationService.createNotification(
|
||||
channel = AndroidNotificationService.NotificationChannels.Tunnel.VPN,
|
||||
title = context.getString(R.string.initializing),
|
||||
onGoing = true,
|
||||
groupKey = VPN_GROUP_KEY,
|
||||
)
|
||||
|
||||
override val proxyInitNotification: Notification
|
||||
get() =
|
||||
notificationService.createNotification(
|
||||
channel = AndroidNotificationService.NotificationChannels.Tunnel.Proxy,
|
||||
title = context.getString(R.string.initializing),
|
||||
onGoing = true,
|
||||
groupKey = PROXY_GROUP_KEY,
|
||||
)
|
||||
|
||||
override val vpnNotificationId: Int
|
||||
get() = NotificationService.VPN_NOTIFICATION_ID
|
||||
|
||||
override val proxyNotificationId: Int
|
||||
get() = NotificationService.PROXY_NOTIFICATION_ID
|
||||
|
||||
override suspend fun buildVpnPersistentNotification(
|
||||
currentStatus: BackendStatus
|
||||
): Notification {
|
||||
val lines = computeVpnNotificationLines(currentStatus)
|
||||
return tunnelNotificationService.buildVpnPersistentNotification(lines)
|
||||
}
|
||||
|
||||
override suspend fun buildProxyPersistentNotification(
|
||||
currentStatus: BackendStatus
|
||||
): Notification {
|
||||
val lines = computeProxyNotificationLines(currentStatus)
|
||||
return tunnelNotificationService.buildProxyPersistentNotification(lines)
|
||||
}
|
||||
|
||||
private suspend fun computeVpnNotificationLines(
|
||||
status: BackendStatus
|
||||
): Map<Int, TunnelNotificationLine> {
|
||||
val activeTunnels = status.activeTunnels
|
||||
val allTunnels = tunnelRepository.userTunnelsFlow.first()
|
||||
return activeTunnels
|
||||
.mapNotNull { (id, activeTunnel) ->
|
||||
val mode = activeTunnel.mode ?: return@mapNotNull null
|
||||
if (mode !is BackendMode.Vpn && mode !is BackendMode.Proxy.KillSwitchPrimary)
|
||||
return@mapNotNull null
|
||||
val tunnel = allTunnels.find { it.id == id } ?: return@mapNotNull null
|
||||
val displayState = DisplayTunnelState.from(activeTunnel)
|
||||
TunnelNotificationLine(id, tunnel.name, displayState)
|
||||
}
|
||||
.associateBy { it.id }
|
||||
}
|
||||
|
||||
private suspend fun computeProxyNotificationLines(
|
||||
status: BackendStatus
|
||||
): Map<Int, TunnelNotificationLine> {
|
||||
val activeTunnels = status.activeTunnels
|
||||
val allTunnels = tunnelRepository.userTunnelsFlow.first()
|
||||
return activeTunnels
|
||||
.mapNotNull { (id, activeTunnel) ->
|
||||
val mode = activeTunnel.mode ?: return@mapNotNull null
|
||||
if (mode !is BackendMode.Proxy.Standard) return@mapNotNull null
|
||||
val tunnel = allTunnels.find { it.id == id } ?: return@mapNotNull null
|
||||
val displayState = DisplayTunnelState.from(activeTunnel)
|
||||
TunnelNotificationLine(id, tunnel.name, displayState)
|
||||
}
|
||||
.associateBy { it.id }
|
||||
}
|
||||
}
|
||||
+3
-8
@@ -4,14 +4,11 @@ import com.zaneschepke.tunnel.Tunnel
|
||||
import com.zaneschepke.tunnel.backend.Backend
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.tunnel.state.BackendStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
@@ -53,9 +50,7 @@ class TunnelBackendProvider(
|
||||
return backend.disableKillSwitch()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val localErrorEvents = MutableSharedFlow<Pair<String?, BackendCoreException>>()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val localMessageEvents = MutableSharedFlow<Pair<String?, BackendMessage>>()
|
||||
override suspend fun setSeamlessRoaming(enabled: Boolean): Result<Unit> {
|
||||
return backend.setSeamlessRoaming(enabled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ interface TunnelProvider {
|
||||
|
||||
suspend fun disableLockDown(): Result<Unit>
|
||||
|
||||
suspend fun setSeamlessRoaming(enabled: Boolean): Result<Unit>
|
||||
|
||||
val backendStatus: StateFlow<BackendStatus>
|
||||
|
||||
val events: Flow<TunnelEvent>
|
||||
|
||||
@@ -6,9 +6,9 @@ import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||
import java.util.concurrent.TimeUnit
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
|
||||
DnsSettings::class,
|
||||
LockdownSettings::class,
|
||||
],
|
||||
version = 30,
|
||||
version = 32,
|
||||
autoMigrations =
|
||||
[
|
||||
AutoMigration(from = 1, to = 2),
|
||||
@@ -63,6 +63,8 @@ import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
|
||||
AutoMigration(from = 26, to = 27, spec = GlobalsMigration::class),
|
||||
AutoMigration(from = 27, to = 28, spec = DonationMigration::class),
|
||||
AutoMigration(from = 29, to = 30, spec = SingleConfigMigration::class),
|
||||
AutoMigration(from = 30, to = 31),
|
||||
AutoMigration(from = 31, to = 32),
|
||||
],
|
||||
exportSchema = true,
|
||||
)
|
||||
|
||||
+3
@@ -18,4 +18,7 @@ interface AutoTunnelSettingsDao {
|
||||
|
||||
@Query("UPDATE auto_tunnel_settings SET is_tunnel_enabled = :enabled")
|
||||
suspend fun updateAutoTunnelEnabled(enabled: Boolean)
|
||||
|
||||
@Query("UPDATE auto_tunnel_settings SET disable_on_captive_portal = :enabled")
|
||||
suspend fun updateDisableOnCaptivePortal(enabled: Boolean)
|
||||
}
|
||||
|
||||
@@ -34,4 +34,7 @@ interface GeneralSettingsDao {
|
||||
|
||||
@Query("UPDATE general_settings SET screen_recording_security = :enabled")
|
||||
suspend fun updateScreenRecordingSecurity(enabled: Boolean)
|
||||
|
||||
@Query("UPDATE general_settings SET seamless_roaming_enabled = :enabled")
|
||||
suspend fun updateSeamlessRoaming(enabled: Boolean)
|
||||
}
|
||||
|
||||
+2
@@ -27,4 +27,6 @@ data class AutoTunnelSettings(
|
||||
@ColumnInfo(name = "wifi_detection_method", defaultValue = "0")
|
||||
val wifiDetectionMethod: WifiDetectionMethod = WifiDetectionMethod.fromValue(0),
|
||||
@ColumnInfo(name = "start_on_boot", defaultValue = "0") val startOnBoot: Boolean = false,
|
||||
@ColumnInfo(name = "disable_on_captive_portal", defaultValue = "1")
|
||||
val disableTunnelOnCaptivePortal: Boolean = true,
|
||||
)
|
||||
|
||||
@@ -34,4 +34,6 @@ data class GeneralSettings(
|
||||
val isGlobalAmneziaEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "tunnel_scripting_enabled", defaultValue = "0")
|
||||
val tunnelScriptingEnabled: Boolean = true,
|
||||
@ColumnInfo(name = "seamless_roaming_enabled", defaultValue = "0")
|
||||
val seamlessRoamingEnabled: Boolean = true,
|
||||
)
|
||||
|
||||
+2
@@ -16,6 +16,7 @@ fun Entity.toDomain(): Domain =
|
||||
isTunnelOnUnsecureEnabled = isTunnelOnUnsecureEnabled,
|
||||
wifiDetectionMethod = wifiDetectionMethod,
|
||||
startOnBoot = startOnBoot,
|
||||
disableTunnelOnCaptivePortal = disableTunnelOnCaptivePortal
|
||||
)
|
||||
|
||||
fun Domain.toEntity(): Entity =
|
||||
@@ -31,4 +32,5 @@ fun Domain.toEntity(): Entity =
|
||||
isTunnelOnUnsecureEnabled = isTunnelOnUnsecureEnabled,
|
||||
wifiDetectionMethod = wifiDetectionMethod,
|
||||
startOnBoot = startOnBoot,
|
||||
disableTunnelOnCaptivePortal = disableTunnelOnCaptivePortal
|
||||
)
|
||||
|
||||
@@ -22,6 +22,7 @@ fun Entity.toDomain(): Domain =
|
||||
screenRecordingSecurityEnabled = screenRecordingSecurityEnabled,
|
||||
isGlobalAmneziaEnabled = isGlobalAmneziaEnabled,
|
||||
tunnelScriptingEnabled = tunnelScriptingEnabled,
|
||||
seamlessRoamingEnabled = seamlessRoamingEnabled,
|
||||
)
|
||||
|
||||
fun Domain.toEntity(): Entity =
|
||||
@@ -42,4 +43,5 @@ fun Domain.toEntity(): Entity =
|
||||
screenRecordingSecurityEnabled = screenRecordingSecurityEnabled,
|
||||
isGlobalAmneziaEnabled = isGlobalAmneziaEnabled,
|
||||
tunnelScriptingEnabled = tunnelScriptingEnabled,
|
||||
seamlessRoamingEnabled = seamlessRoamingEnabled,
|
||||
)
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.network
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.okhttp.OkHttp
|
||||
import io.ktor.client.plugins.DefaultRequest
|
||||
import io.ktor.client.plugins.HttpTimeout
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
object KtorClient {
|
||||
fun create(): HttpClient {
|
||||
return HttpClient(OkHttp) {
|
||||
install(DefaultRequest) {
|
||||
headers {
|
||||
append(HttpHeaders.UserAgent, "wgtunnel/${BuildConfig.VERSION_NAME} (Android)")
|
||||
append(HttpHeaders.Accept, "*/*")
|
||||
}
|
||||
}
|
||||
|
||||
install(ContentNegotiation) {
|
||||
json(
|
||||
Json {
|
||||
@@ -18,10 +28,11 @@ object KtorClient {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
install(HttpTimeout) {
|
||||
requestTimeoutMillis = 15000
|
||||
connectTimeoutMillis = 15000
|
||||
socketTimeoutMillis = 15000
|
||||
requestTimeoutMillis = 120_000L
|
||||
connectTimeoutMillis = 30_000L
|
||||
socketTimeoutMillis = 120_000L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -26,4 +26,8 @@ class RoomAutoTunnelSettingsRepository(private val autoTunnelSettingsDao: AutoTu
|
||||
override suspend fun updateAutoTunnelEnabled(enabled: Boolean) {
|
||||
autoTunnelSettingsDao.updateAutoTunnelEnabled(enabled)
|
||||
}
|
||||
|
||||
override suspend fun updateDisableOnCaptivePortal(enabled: Boolean) {
|
||||
autoTunnelSettingsDao.updateDisableOnCaptivePortal(enabled)
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -45,4 +45,8 @@ class RoomSettingsRepository(private val settingsDao: GeneralSettingsDao) :
|
||||
override suspend fun updateScreenRecordingSecurity(enabled: Boolean) {
|
||||
settingsDao.updateScreenRecordingSecurity(enabled)
|
||||
}
|
||||
|
||||
override suspend fun updateSeamlessRoaming(enabled: Boolean) {
|
||||
settingsDao.updateSeamlessRoaming(enabled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,20 +6,19 @@ import android.os.StrictMode
|
||||
import com.zaneschepke.logcatter.LogReader
|
||||
import com.zaneschepke.logcatter.LogcatReader
|
||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.DynamicShortcutManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.SelectedTunnelsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigEditViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.DnsViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.LicenseViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.LockdownViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.LoggerViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.MonitoringViewModel
|
||||
@@ -80,7 +79,6 @@ val appModule = module {
|
||||
viewModelOf(::AutoTunnelViewModel)
|
||||
viewModel { (id: Int?) -> ConfigEditViewModel(get(), get(), get(), get(), get(), id) }
|
||||
viewModelOf(::DnsViewModel)
|
||||
viewModelOf(::LicenseViewModel)
|
||||
viewModelOf(::LockdownViewModel)
|
||||
viewModelOf(::LoggerViewModel)
|
||||
viewModelOf(::MonitoringViewModel)
|
||||
|
||||
@@ -5,15 +5,15 @@ import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordina
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.DnsSettingsCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.ShortcutCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.StartupCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelBackendCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelModeCoordinator
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val coordinatorModule = module {
|
||||
singleOf(::ShortcutCoordinator)
|
||||
singleOf(::TunnelModeCoordinator)
|
||||
singleOf(::TunnelBackendCoordinator)
|
||||
singleOf(::StartupCoordinator)
|
||||
singleOf(::AutoTunnelCoordinator)
|
||||
singleOf(::DnsSettingsCoordinator)
|
||||
@@ -27,6 +27,7 @@ val coordinatorModule = module {
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(named(Scope.APPLICATION)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
package com.zaneschepke.wireguardautotunnel.di
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Context
|
||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
||||
import com.zaneschepke.tunnel.NotificationProvider
|
||||
import com.zaneschepke.tunnel.backend.RootShell
|
||||
import com.zaneschepke.tunnel.ApplicationProvider
|
||||
import com.zaneschepke.tunnel.util.RootShell
|
||||
import com.zaneschepke.tunnel.util.RootShellException
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.event.TunnelEventDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService.NotificationChannels
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidTunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.PROXY_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.VPN_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.TunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.AndroidApplicationProvider
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelBackendProvider
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.lifecyle.AppVisibilityObserver
|
||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidTunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.to
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -36,40 +30,15 @@ import timber.log.Timber
|
||||
|
||||
val tunnelBackendProviderModule = module {
|
||||
single<TunnelNotificationService> { AndroidTunnelNotificationService(get()) }
|
||||
single { AppVisibilityObserver() }
|
||||
singleOf(::TunnelEventDispatcher)
|
||||
|
||||
single<NotificationProvider> {
|
||||
val notificationService = get<NotificationService>()
|
||||
val context = androidContext()
|
||||
object : NotificationProvider {
|
||||
override val vpnInitNotification: Notification
|
||||
get() =
|
||||
notificationService.createNotification(
|
||||
channel = NotificationChannels.Tunnel.VPN,
|
||||
title = context.getString(R.string.initializing),
|
||||
onGoing = true,
|
||||
groupKey = VPN_GROUP_KEY,
|
||||
)
|
||||
|
||||
override val proxyInitNotification: Notification
|
||||
get() =
|
||||
notificationService.createNotification(
|
||||
channel = NotificationChannels.Tunnel.Proxy,
|
||||
title = context.getString(R.string.initializing),
|
||||
onGoing = true,
|
||||
groupKey = PROXY_GROUP_KEY,
|
||||
)
|
||||
|
||||
override val vpnNotificationId: Int
|
||||
get() = NotificationService.VPN_NOTIFICATION_ID
|
||||
|
||||
override val proxyNotificationId: Int
|
||||
get() = NotificationService.PROXY_NOTIFICATION_ID
|
||||
|
||||
override fun refreshTile(context: Context) {
|
||||
TunnelTileRefresher.refresh(context)
|
||||
}
|
||||
}
|
||||
single<ApplicationProvider> {
|
||||
AndroidApplicationProvider(
|
||||
notificationService = get(),
|
||||
tunnelNotificationService = get(),
|
||||
tunnelRepository = get(),
|
||||
)
|
||||
}
|
||||
|
||||
single {
|
||||
|
||||
+1
@@ -14,4 +14,5 @@ data class AutoTunnelSettings(
|
||||
val isTunnelOnUnsecureEnabled: Boolean = false,
|
||||
val wifiDetectionMethod: WifiDetectionMethod = WifiDetectionMethod.fromValue(0),
|
||||
val startOnBoot: Boolean = false,
|
||||
val disableTunnelOnCaptivePortal: Boolean = true,
|
||||
)
|
||||
|
||||
+2
-1
@@ -20,5 +20,6 @@ data class GeneralSettings(
|
||||
val alreadyDonated: Boolean = false,
|
||||
val screenRecordingSecurityEnabled: Boolean = true,
|
||||
val isGlobalAmneziaEnabled: Boolean = false,
|
||||
val tunnelScriptingEnabled: Boolean = true,
|
||||
val tunnelScriptingEnabled: Boolean = false,
|
||||
val seamlessRoamingEnabled: Boolean = false,
|
||||
)
|
||||
|
||||
+2
@@ -11,4 +11,6 @@ interface AutoTunnelSettingsRepository {
|
||||
suspend fun getAutoTunnelSettings(): AutoTunnelSettings
|
||||
|
||||
suspend fun updateAutoTunnelEnabled(enabled: Boolean)
|
||||
|
||||
suspend fun updateDisableOnCaptivePortal(enabled: Boolean)
|
||||
}
|
||||
|
||||
+2
@@ -23,4 +23,6 @@ interface GeneralSettingRepository {
|
||||
suspend fun updateGlobalAmneziaEnabled(enabled: Boolean)
|
||||
|
||||
suspend fun updateScreenRecordingSecurity(enabled: Boolean)
|
||||
|
||||
suspend fun updateSeamlessRoaming(enabled: Boolean)
|
||||
}
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.asSharedFlow
|
||||
class GlobalEffectRepository {
|
||||
|
||||
private val _globalEffectFlow =
|
||||
MutableSharedFlow<GlobalSideEffect>(replay = 0, extraBufferCapacity = 1)
|
||||
MutableSharedFlow<GlobalSideEffect>(replay = 0, extraBufferCapacity = 0)
|
||||
val flow = _globalEffectFlow.asSharedFlow()
|
||||
|
||||
suspend fun post(effect: GlobalSideEffect) {
|
||||
|
||||
+2
-4
@@ -1,8 +1,8 @@
|
||||
package com.zaneschepke.wireguardautotunnel.domain.sideeffect
|
||||
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarType
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import java.io.File
|
||||
|
||||
@@ -10,14 +10,12 @@ sealed class GlobalSideEffect {
|
||||
|
||||
data class Snackbar(
|
||||
val message: StringValue,
|
||||
val type: SnackbarType? = null,
|
||||
val type: ToastType,
|
||||
val actionLabel: String? = null,
|
||||
val onAction: (() -> Unit)? = null,
|
||||
val durationMs: Long? = null,
|
||||
) : GlobalSideEffect()
|
||||
|
||||
data class Toast(val message: StringValue) : GlobalSideEffect()
|
||||
|
||||
data object PopBackStack : GlobalSideEffect()
|
||||
|
||||
data class LaunchUrl(val url: String) : GlobalSideEffect()
|
||||
|
||||
@@ -11,16 +11,20 @@ sealed class ActiveNetwork {
|
||||
|
||||
data object Cellular : ActiveNetwork()
|
||||
|
||||
data class Wifi(val ssid: String, val isSecure: Boolean?) : ActiveNetwork()
|
||||
data class Wifi(
|
||||
val ssid: String,
|
||||
val isSecure: Boolean?,
|
||||
val requiresCaptivePortalLogin: Boolean,
|
||||
) : ActiveNetwork()
|
||||
}
|
||||
|
||||
data class NetworkState(
|
||||
val activeNetwork: ActiveNetwork = ActiveNetwork.Disconnected,
|
||||
val locationServicesEnabled: Boolean = false,
|
||||
val locationPermissionGranted: Boolean = false,
|
||||
) {
|
||||
fun hasInternet(): Boolean = activeNetwork !is ActiveNetwork.Disconnected
|
||||
}
|
||||
// Has a network that can actually transfer data (not suspended)
|
||||
val hasUsableNetwork: Boolean = false,
|
||||
)
|
||||
|
||||
fun ConnectivityState.toDomain(): NetworkState {
|
||||
val domainNetwork: ActiveNetwork =
|
||||
@@ -33,7 +37,11 @@ fun ConnectivityState.toDomain(): NetworkState {
|
||||
null -> null
|
||||
else -> true
|
||||
}
|
||||
ActiveNetwork.Wifi(ssid = network.ssid, isSecure = isSecure)
|
||||
ActiveNetwork.Wifi(
|
||||
ssid = network.ssid,
|
||||
isSecure = isSecure,
|
||||
requiresCaptivePortalLogin(),
|
||||
)
|
||||
}
|
||||
is MonitorActiveNetwork.Cellular -> ActiveNetwork.Cellular
|
||||
is MonitorActiveNetwork.Ethernet -> ActiveNetwork.Ethernet
|
||||
@@ -44,5 +52,6 @@ fun ConnectivityState.toDomain(): NetworkState {
|
||||
activeNetwork = domainNetwork,
|
||||
locationPermissionGranted = this.locationPermissionsGranted,
|
||||
locationServicesEnabled = this.locationServicesEnabled,
|
||||
hasUsableNetwork = hasUsableNetwork(),
|
||||
)
|
||||
}
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.zaneschepke.wireguardautotunnel.lifecyle
|
||||
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
class AppVisibilityObserver : DefaultLifecycleObserver {
|
||||
|
||||
private val _isForeground = MutableStateFlow(false)
|
||||
val isForeground: StateFlow<Boolean> = _isForeground.asStateFlow()
|
||||
|
||||
init {
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||
}
|
||||
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
_isForeground.value = true
|
||||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
_isForeground.value = false
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
package com.zaneschepke.wireguardautotunnel.notification
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Notification
|
||||
@@ -17,8 +17,8 @@ import androidx.core.app.NotificationManagerCompat.IMPORTANCE_HIGH
|
||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.broadcast.NotificationActionReceiver
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.EXTRA_ID
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.EXTRA_ID
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
|
||||
class AndroidNotificationService(override val context: Context) : NotificationService {
|
||||
+130
-25
@@ -1,21 +1,116 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
package com.zaneschepke.wireguardautotunnel.notification
|
||||
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService.NotificationChannels
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.PROXY_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.PROXY_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.TUNNEL_ERROR_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.TUNNEL_MESSAGES_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.VPN_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.VPN_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService.NotificationChannels
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.PROXY_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.PROXY_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.TUNNEL_ERROR_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.TUNNEL_MESSAGES_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.VPN_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.VPN_NOTIFICATION_ID
|
||||
|
||||
class AndroidTunnelNotificationService(private val notificationService: NotificationService) :
|
||||
TunnelNotificationService {
|
||||
|
||||
private val context = notificationService.context
|
||||
|
||||
private fun createGroupNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>,
|
||||
channel: NotificationChannels.Tunnel,
|
||||
groupKey: String,
|
||||
): android.app.Notification {
|
||||
val title =
|
||||
if (tunnelNotificationLines.size == 1) {
|
||||
val name = tunnelNotificationLines.values.first().name
|
||||
when (channel) {
|
||||
is NotificationChannels.Tunnel.VPN ->
|
||||
"${context.getString(R.string.vpn)} • $name"
|
||||
is NotificationChannels.Tunnel.Proxy ->
|
||||
"${context.getString(R.string.proxy)} • $name"
|
||||
}
|
||||
} else {
|
||||
when (channel) {
|
||||
is NotificationChannels.Tunnel.VPN -> context.getString(R.string.vpn)
|
||||
is NotificationChannels.Tunnel.Proxy -> context.getString(R.string.proxy)
|
||||
}
|
||||
}
|
||||
|
||||
val formattedLines =
|
||||
tunnelNotificationLines.values.map { line ->
|
||||
val status = line.displayState.asLocalizedString(context)
|
||||
|
||||
if (tunnelNotificationLines.size == 1) {
|
||||
status
|
||||
} else {
|
||||
context.getString(R.string.notification_tunnel_status_format, line.name, status)
|
||||
}
|
||||
}
|
||||
val description = formattedLines.joinToString("\n")
|
||||
|
||||
val actions =
|
||||
if (tunnelNotificationLines.size == 1) {
|
||||
val tunnelId = tunnelNotificationLines.keys.first()
|
||||
listOf(
|
||||
notificationService.createNotificationAction(
|
||||
notificationAction = NotificationAction.TUNNEL_OFF,
|
||||
extraId = tunnelId,
|
||||
)
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
notificationService.createNotificationAction(
|
||||
notificationAction = NotificationAction.STOP_ALL,
|
||||
extraId = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val style =
|
||||
if (tunnelNotificationLines.size > 1) {
|
||||
NotificationCompat.InboxStyle()
|
||||
.setBigContentTitle(title)
|
||||
.setSummaryText(
|
||||
"${tunnelNotificationLines.size} ${context.getString(R.string.tunnels).lowercase()}"
|
||||
)
|
||||
.also { inbox -> formattedLines.forEach { inbox.addLine(it) } }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return notificationService.createNotification(
|
||||
channel = channel,
|
||||
title = title,
|
||||
description = description,
|
||||
actions = actions,
|
||||
onGoing = true,
|
||||
onlyAlertOnce = true,
|
||||
groupKey = groupKey,
|
||||
style = style,
|
||||
)
|
||||
}
|
||||
|
||||
override fun buildVpnPersistentNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||
): android.app.Notification {
|
||||
return createGroupNotification(
|
||||
tunnelNotificationLines,
|
||||
NotificationChannels.Tunnel.VPN,
|
||||
VPN_GROUP_KEY,
|
||||
)
|
||||
}
|
||||
|
||||
override fun buildProxyPersistentNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||
): android.app.Notification {
|
||||
return createGroupNotification(
|
||||
tunnelNotificationLines,
|
||||
NotificationChannels.Tunnel.Proxy,
|
||||
PROXY_GROUP_KEY,
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateGroupNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>,
|
||||
notificationId: Int,
|
||||
@@ -88,26 +183,36 @@ class AndroidTunnelNotificationService(private val notificationService: Notifica
|
||||
notificationService.show(notificationId, notification)
|
||||
}
|
||||
|
||||
override fun updateProxyPersistentNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||
) {
|
||||
updateGroupNotification(
|
||||
tunnelNotificationLines = tunnelNotificationLines,
|
||||
notificationId = PROXY_NOTIFICATION_ID,
|
||||
channel = NotificationChannels.Tunnel.Proxy,
|
||||
groupKey = PROXY_GROUP_KEY,
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateVpnPersistentNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||
) {
|
||||
updateGroupNotification(
|
||||
tunnelNotificationLines = tunnelNotificationLines,
|
||||
notificationId = VPN_NOTIFICATION_ID,
|
||||
channel = NotificationChannels.Tunnel.VPN,
|
||||
groupKey = VPN_GROUP_KEY,
|
||||
)
|
||||
if (tunnelNotificationLines.isEmpty()) {
|
||||
notificationService.remove(VPN_NOTIFICATION_ID)
|
||||
return
|
||||
}
|
||||
val notification =
|
||||
createGroupNotification(
|
||||
tunnelNotificationLines,
|
||||
NotificationChannels.Tunnel.VPN,
|
||||
VPN_GROUP_KEY,
|
||||
)
|
||||
notificationService.show(VPN_NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
override fun updateProxyPersistentNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||
) {
|
||||
if (tunnelNotificationLines.isEmpty()) {
|
||||
notificationService.remove(PROXY_NOTIFICATION_ID)
|
||||
return
|
||||
}
|
||||
val notification =
|
||||
createGroupNotification(
|
||||
tunnelNotificationLines,
|
||||
NotificationChannels.Tunnel.Proxy,
|
||||
PROXY_GROUP_KEY,
|
||||
)
|
||||
notificationService.show(PROXY_NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
override fun showIpv4Fallback(tunnelName: String) {
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
package com.zaneschepke.wireguardautotunnel.notification
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService.NotificationChannels
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService.NotificationChannels
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
|
||||
interface NotificationService {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
package com.zaneschepke.wireguardautotunnel.notification
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||
|
||||
+11
-1
@@ -1,4 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
package com.zaneschepke.wireguardautotunnel.notification
|
||||
|
||||
import android.app.Notification
|
||||
|
||||
interface TunnelNotificationService {
|
||||
|
||||
@@ -6,6 +8,14 @@ interface TunnelNotificationService {
|
||||
|
||||
fun updateVpnPersistentNotification(tunnelNotificationLines: Map<Int, TunnelNotificationLine>)
|
||||
|
||||
fun buildVpnPersistentNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||
): Notification
|
||||
|
||||
fun buildProxyPersistentNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||
): Notification
|
||||
|
||||
fun showIpv4Fallback(tunnelName: String)
|
||||
|
||||
fun showIpv6Recovery(tunnelName: String)
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service
|
||||
package com.zaneschepke.wireguardautotunnel.service
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
|
||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelService
|
||||
|
||||
class ServiceManager(private val context: Context) {
|
||||
|
||||
+14
-2
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||
package com.zaneschepke.wireguardautotunnel.service.autotunnel
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
@@ -28,7 +28,19 @@ class AutoTunnelEngine {
|
||||
|
||||
val activeTunnelIds = backend.activeTunnels.keys.toSet()
|
||||
|
||||
if (!network.hasInternet()) {
|
||||
val isOnCaptivePortalWifi =
|
||||
network.activeNetwork is ActiveNetwork.Wifi &&
|
||||
network.activeNetwork.requiresCaptivePortalLogin
|
||||
|
||||
if (isOnCaptivePortalWifi && settings.disableTunnelOnCaptivePortal) {
|
||||
return if (activeTunnelIds.isNotEmpty()) {
|
||||
Decision.Sync(start = emptySet(), stop = activeTunnelIds)
|
||||
} else {
|
||||
Decision.None
|
||||
}
|
||||
}
|
||||
|
||||
if (!network.hasUsableNetwork) {
|
||||
return if (settings.isStopOnNoInternetEnabled) {
|
||||
Decision.StopDueToNoInternet
|
||||
} else {
|
||||
+12
-8
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||
package com.zaneschepke.wireguardautotunnel.service.autotunnel
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.core.app.ServiceCompat
|
||||
@@ -7,10 +7,7 @@ import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.di.Dispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelActionSource
|
||||
@@ -23,17 +20,21 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepos
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.toDomain
|
||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.to
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
@@ -77,13 +78,16 @@ class AutoTunnelService : LifecycleService() {
|
||||
@Volatile private var hasUserOverride = false
|
||||
private var lastNetworkFingerprint: AutoTunnelState.NetworkFingerprint? = null
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
private val autoTunnelStateFlow: Flow<AutoTunnelState> by lazy {
|
||||
val networkFlow = networkEngine.stableState.mapNotNull { it?.state?.toDomain() }
|
||||
|
||||
val settingsFlow = combineSettings()
|
||||
|
||||
val backendFlow =
|
||||
tunnelCoordinator.backendStatus.distinctUntilChangedBy { it.activeTunnels.keys.toSet() }
|
||||
tunnelCoordinator.backendStatus
|
||||
.distinctUntilChanged { old, new -> old.activeTunnels == new.activeTunnels }
|
||||
.debounce(300L.milliseconds)
|
||||
|
||||
combine(networkFlow, settingsFlow, backendFlow) { network, settings, backend ->
|
||||
AutoTunnelState(
|
||||
@@ -188,11 +192,11 @@ class AutoTunnelService : LifecycleService() {
|
||||
reconciliationMutex.withLock {
|
||||
val currentNetworkState = networkEngine.stableState.value?.state?.toDomain()
|
||||
|
||||
val stillNoInternet = currentNetworkState?.hasInternet() == false
|
||||
val stillNoUsableNetwork = currentNetworkState?.hasUsableNetwork == false
|
||||
val stopOnNoInternetEnabled =
|
||||
autoTunnelRepository.flow.firstOrNull()?.isStopOnNoInternetEnabled == true
|
||||
|
||||
if (stillNoInternet && stopOnNoInternetEnabled) {
|
||||
if (stillNoUsableNetwork && stopOnNoInternetEnabled) {
|
||||
val currentActiveIds =
|
||||
tunnelCoordinator.backendStatus.value.activeTunnels.keys
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||
package com.zaneschepke.wireguardautotunnel.service.autotunnel
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
||||
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
||||
|
||||
import android.content.Context
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
||||
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.Tile
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.dialog
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
|
||||
@Composable
|
||||
fun LocalNetworkPermissionDialog(onDismiss: () -> Unit, onAttest: () -> Unit) {
|
||||
InfoDialog(
|
||||
onAttest = onAttest,
|
||||
onDismiss = onDismiss,
|
||||
title = stringResource(R.string.local_network_permission_title),
|
||||
body = {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.local_network_permission_intro),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.local_network_permission_issues_intro),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.local_network_permission_feature_tunnels),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.local_network_permission_feature_autotunnel),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.local_network_permission_feature_proxy),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.local_network_permission_recommendation),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.local_network_permission_nearby_devices),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmText = stringResource(R.string._continue),
|
||||
)
|
||||
}
|
||||
-104
@@ -1,104 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Favorite
|
||||
import androidx.compose.material.icons.rounded.Close
|
||||
import androidx.compose.material.icons.rounded.Info
|
||||
import androidx.compose.material.icons.rounded.Warning
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Snackbar
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
|
||||
|
||||
@Composable
|
||||
fun CustomSnackBar(
|
||||
message: AnnotatedString,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
type: SnackbarType = SnackbarType.INFO,
|
||||
containerColor: Color = MaterialTheme.colorScheme.surface,
|
||||
) {
|
||||
val isTv = LocalIsAndroidTV.current
|
||||
val icon =
|
||||
when (type) {
|
||||
SnackbarType.INFO -> Icons.Rounded.Info
|
||||
SnackbarType.WARNING -> Icons.Rounded.Warning
|
||||
SnackbarType.THANK_YOU -> Icons.Outlined.Favorite
|
||||
}
|
||||
val iconDescription =
|
||||
when (type) {
|
||||
SnackbarType.INFO -> stringResource(R.string.info)
|
||||
SnackbarType.WARNING -> stringResource(R.string.warning)
|
||||
SnackbarType.THANK_YOU -> stringResource(R.string.thank_you)
|
||||
}
|
||||
|
||||
Snackbar(
|
||||
containerColor = containerColor,
|
||||
modifier =
|
||||
modifier
|
||||
.wrapContentHeight(align = Alignment.Top)
|
||||
.padding(horizontal = if (isTv) 48.dp else 16.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.height(IntrinsicSize.Min)
|
||||
.width(IntrinsicSize.Min)
|
||||
.padding(vertical = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().weight(1f),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = iconDescription,
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Text(
|
||||
text = message,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 8,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
Row {
|
||||
IconButton(onClick = onDismiss, modifier = Modifier.size(24.dp)) {
|
||||
Icon(
|
||||
Icons.Rounded.Close,
|
||||
contentDescription = stringResource(R.string.stop),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-76
@@ -1,76 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun rememberCustomSnackbarState(): CustomSnackbarState {
|
||||
return remember { CustomSnackbarState() }
|
||||
}
|
||||
|
||||
class CustomSnackbarState {
|
||||
private val _snackbars = Channel<SnackbarInfo>(Channel.BUFFERED)
|
||||
val snackbars: Channel<SnackbarInfo> = _snackbars
|
||||
|
||||
private var currentSnackbar by mutableStateOf<SnackbarInfo?>(null)
|
||||
private var isShowing by mutableStateOf(false)
|
||||
|
||||
fun showSnackbar(info: SnackbarInfo) {
|
||||
_snackbars.trySend(info)
|
||||
}
|
||||
|
||||
fun dismissCurrent() {
|
||||
currentSnackbar = null
|
||||
isShowing = false
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SnackbarHost(
|
||||
modifier: Modifier = Modifier,
|
||||
snackbar: @Composable (SnackbarInfo) -> Unit = { info ->
|
||||
CustomSnackBar(
|
||||
message = info.message,
|
||||
type = info.type,
|
||||
onDismiss = { dismissCurrent() },
|
||||
modifier = Modifier,
|
||||
containerColor = MaterialTheme.colorScheme.surface.copy(.1f),
|
||||
)
|
||||
},
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
LaunchedEffect(Unit) {
|
||||
for (info in snackbars) {
|
||||
currentSnackbar = info
|
||||
isShowing = true
|
||||
|
||||
scope.launch {
|
||||
delay(info.durationMs)
|
||||
if (currentSnackbar?.id == info.id) {
|
||||
dismissCurrent()
|
||||
}
|
||||
}
|
||||
|
||||
while (isShowing && currentSnackbar?.id == info.id) {
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentSnackbar?.let { info ->
|
||||
if (isShowing) {
|
||||
Box(modifier = modifier) { snackbar(info) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
|
||||
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
|
||||
enum class SnackbarType {
|
||||
INFO,
|
||||
WARNING,
|
||||
THANK_YOU,
|
||||
}
|
||||
|
||||
data class SnackbarInfo(
|
||||
val message: AnnotatedString,
|
||||
val type: SnackbarType = SnackbarType.INFO,
|
||||
val durationMs: Long = 4000L,
|
||||
val id: String = System.currentTimeMillis().toString(),
|
||||
)
|
||||
+16
@@ -10,6 +10,7 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Filter1
|
||||
import androidx.compose.material.icons.outlined.Map
|
||||
import androidx.compose.material.icons.outlined.PublicOff
|
||||
import androidx.compose.material.icons.outlined.WifiFind
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -191,6 +192,21 @@ fun WifiSettingsScreen(viewModel: AutoTunnelViewModel = koinViewModel()) {
|
||||
)
|
||||
},
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.PublicOff, contentDescription = null) },
|
||||
title = stringResource(R.string.stop_while_captive_portal),
|
||||
onClick = {
|
||||
viewModel.setDisabledOnCaptivePortal(
|
||||
!uiState.autoTunnelSettings.disableTunnelOnCaptivePortal
|
||||
)
|
||||
},
|
||||
trailing = {
|
||||
ThemedSwitch(
|
||||
checked = uiState.autoTunnelSettings.disableTunnelOnCaptivePortal,
|
||||
onClick = { viewModel.setDisabledOnCaptivePortal(it) },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
Column {
|
||||
GroupLabel(stringResource(R.string.tunnels), Modifier.padding(horizontal = 16.dp))
|
||||
|
||||
+9
-2
@@ -9,6 +9,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||
@@ -45,11 +46,17 @@ fun PinLockScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
||||
textColor = MaterialTheme.colorScheme.onSurface,
|
||||
onPinCorrect = { onPinCorrect() },
|
||||
onPinIncorrect = {
|
||||
sharedViewModel.showToast(StringValue.StringResource(R.string.incorrect_pin))
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.incorrect_pin),
|
||||
ToastType.Warning,
|
||||
)
|
||||
},
|
||||
onPinCreated = {
|
||||
pinCreated = true
|
||||
sharedViewModel.showToast(StringValue.StringResource(R.string.pin_created))
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.pin_created),
|
||||
ToastType.Success,
|
||||
)
|
||||
sharedViewModel.setPinLockEnabled(true)
|
||||
onPinCorrect()
|
||||
},
|
||||
|
||||
+67
-10
@@ -9,6 +9,7 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ViewQuilt
|
||||
import androidx.compose.material.icons.outlined.Android
|
||||
import androidx.compose.material.icons.outlined.CellWifi
|
||||
import androidx.compose.material.icons.outlined.Dns
|
||||
import androidx.compose.material.icons.outlined.ExpandMore
|
||||
import androidx.compose.material.icons.outlined.MonitorHeart
|
||||
@@ -38,6 +39,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
@@ -51,12 +53,12 @@ import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackupBottomSheet
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackupEncryptionDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.proxy.compoents.AppModeBottomSheet
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asString
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.capitalize
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
@@ -88,6 +90,9 @@ fun SettingsScreen(
|
||||
}
|
||||
|
||||
var showBackupSheet by rememberSaveable { mutableStateOf(false) }
|
||||
var showEncryptionDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var isRestoreAction by remember { mutableStateOf(false) }
|
||||
|
||||
var showAppModeSheet by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val appMode = uiState.settings.tunnelMode
|
||||
@@ -99,19 +104,53 @@ fun SettingsScreen(
|
||||
}
|
||||
|
||||
fun performBackupRestore(action: () -> Unit) {
|
||||
if (uiState.tunnelActive || globalUiState.isAutoTunnelActive)
|
||||
return context.showToast(R.string.all_services_disabled)
|
||||
showBackupSheet = false
|
||||
if (uiState.tunnelActive || globalUiState.isAutoTunnelActive) {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.all_services_disabled),
|
||||
ToastType.Warning,
|
||||
)
|
||||
return
|
||||
}
|
||||
action()
|
||||
}
|
||||
|
||||
if (showBackupSheet)
|
||||
if (showBackupSheet) {
|
||||
BackupBottomSheet(
|
||||
{ performBackupRestore { (context as? MainActivity)?.performBackup() } },
|
||||
{ performBackupRestore { (context as? MainActivity)?.performRestore() } },
|
||||
) {
|
||||
showBackupSheet = false
|
||||
}
|
||||
onBackup = {
|
||||
showBackupSheet = false
|
||||
isRestoreAction = false
|
||||
showEncryptionDialog = true
|
||||
},
|
||||
onRestore = {
|
||||
showBackupSheet = false
|
||||
isRestoreAction = true
|
||||
showEncryptionDialog = true
|
||||
},
|
||||
onDismiss = { showBackupSheet = false },
|
||||
)
|
||||
}
|
||||
|
||||
if (showEncryptionDialog) {
|
||||
BackupEncryptionDialog(
|
||||
isRestore = isRestoreAction,
|
||||
onConfirm = { encrypt, password ->
|
||||
showEncryptionDialog = false
|
||||
|
||||
if (isRestoreAction) {
|
||||
performBackupRestore {
|
||||
(context as? MainActivity)?.performRestore(encrypt, password)
|
||||
}
|
||||
} else {
|
||||
performBackupRestore {
|
||||
(context as? MainActivity)?.performBackup(encrypt, password)
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismiss = { showEncryptionDialog = false },
|
||||
)
|
||||
}
|
||||
|
||||
if (showAppModeSheet)
|
||||
AppModeBottomSheet(sharedViewModel::setAppMode, uiState.settings.tunnelMode) {
|
||||
showAppModeSheet = false
|
||||
@@ -168,7 +207,8 @@ fun SettingsScreen(
|
||||
StringValue.StringResource(
|
||||
R.string.mode_disabled_template,
|
||||
appMode.asString(context),
|
||||
)
|
||||
),
|
||||
ToastType.Info,
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -195,6 +235,23 @@ fun SettingsScreen(
|
||||
viewModel.setTunnelScriptedEnabled(!uiState.settings.tunnelScriptingEnabled)
|
||||
},
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.CellWifi, contentDescription = null) },
|
||||
title = stringResource(R.string.seamless_roaming),
|
||||
trailing = { modifier ->
|
||||
ThemedSwitch(
|
||||
checked = uiState.settings.seamlessRoamingEnabled,
|
||||
onClick = { viewModel.setSeamlessNetworkRoaming(enabled = it) },
|
||||
modifier = modifier,
|
||||
)
|
||||
},
|
||||
description = {
|
||||
DescriptionText(stringResource(R.string.seamless_roaming_description))
|
||||
},
|
||||
onClick = {
|
||||
viewModel.setSeamlessNetworkRoaming(!uiState.settings.seamlessRoamingEnabled)
|
||||
},
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.MonitorHeart, null) },
|
||||
title = stringResource(R.string.tunnel_monitoring),
|
||||
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Visibility
|
||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField
|
||||
|
||||
@Composable
|
||||
fun BackupEncryptionDialog(
|
||||
isRestore: Boolean,
|
||||
onConfirm: (encrypt: Boolean, password: String?) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
var encrypt by remember { mutableStateOf(false) }
|
||||
var password by remember { mutableStateOf("") }
|
||||
var confirmPassword by remember { mutableStateOf("") }
|
||||
var showPasswordError by remember { mutableStateOf(false) }
|
||||
var passwordVisible by remember { mutableStateOf(false) }
|
||||
var confirmPasswordVisible by remember { mutableStateOf(false) }
|
||||
|
||||
InfoDialog(
|
||||
title =
|
||||
if (isRestore) {
|
||||
stringResource(R.string.restore)
|
||||
} else {
|
||||
stringResource(R.string.backup)
|
||||
},
|
||||
confirmText =
|
||||
if (isRestore) {
|
||||
stringResource(R.string.restore)
|
||||
} else {
|
||||
stringResource(R.string.backup)
|
||||
},
|
||||
onAttest = {
|
||||
if (!isRestore && encrypt && password != confirmPassword) {
|
||||
showPasswordError = true
|
||||
return@InfoDialog
|
||||
}
|
||||
if (encrypt && password.isBlank()) {
|
||||
return@InfoDialog
|
||||
}
|
||||
|
||||
val finalPassword = if (encrypt) password else null
|
||||
onConfirm(encrypt, finalPassword)
|
||||
},
|
||||
onDismiss = onDismiss,
|
||||
body = {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(stringResource(R.string.encrypted))
|
||||
ThemedSwitch(checked = encrypt, onClick = { encrypt = it })
|
||||
}
|
||||
|
||||
if (encrypt) {
|
||||
CustomTextField(
|
||||
value = password,
|
||||
onValueChange = {
|
||||
password = it
|
||||
showPasswordError = false
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
label = { Text(stringResource(R.string.password)) },
|
||||
visualTransformation =
|
||||
if (passwordVisible) VisualTransformation.None
|
||||
else PasswordVisualTransformation(),
|
||||
trailing = {
|
||||
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (passwordVisible) Icons.Outlined.VisibilityOff
|
||||
else Icons.Outlined.Visibility,
|
||||
contentDescription =
|
||||
if (passwordVisible) stringResource(R.string.hide_password)
|
||||
else stringResource(R.string.show_password),
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
if (!isRestore) {
|
||||
CustomTextField(
|
||||
value = confirmPassword,
|
||||
onValueChange = {
|
||||
confirmPassword = it
|
||||
showPasswordError = false
|
||||
},
|
||||
label = { Text(stringResource(R.string.confirm_password)) },
|
||||
visualTransformation =
|
||||
if (confirmPasswordVisible) VisualTransformation.None
|
||||
else PasswordVisualTransformation(),
|
||||
trailing = {
|
||||
IconButton(
|
||||
onClick = { confirmPasswordVisible = !confirmPasswordVisible }
|
||||
) {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (confirmPasswordVisible) Icons.Outlined.VisibilityOff
|
||||
else Icons.Outlined.Visibility,
|
||||
contentDescription =
|
||||
if (confirmPasswordVisible)
|
||||
stringResource(R.string.hide_password)
|
||||
else stringResource(R.string.show_password),
|
||||
)
|
||||
}
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isError = showPasswordError,
|
||||
)
|
||||
}
|
||||
|
||||
if (showPasswordError) {
|
||||
Text(
|
||||
text = stringResource(R.string.passwords_do_not_match),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
+5
-2
@@ -12,6 +12,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.components.LogList
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.components.LogsBottomSheet
|
||||
@@ -86,13 +87,15 @@ fun LogsScreen(
|
||||
},
|
||||
onCanceled = {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.export_canceled)
|
||||
StringValue.StringResource(R.string.export_canceled),
|
||||
ToastType.Warning,
|
||||
)
|
||||
showLogsSheet = false
|
||||
},
|
||||
onUnsupported = {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.export_unsupported)
|
||||
StringValue.StringResource(R.string.export_unsupported),
|
||||
ToastType.Warning,
|
||||
)
|
||||
showLogsSheet = false
|
||||
},
|
||||
|
||||
+67
-15
@@ -26,6 +26,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -37,8 +38,10 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
|
||||
@@ -49,12 +52,13 @@ import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.PermissionDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.UpdateDialog
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchPlayStoreListing
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchPlayStoreReview
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchSupportEmail
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SupportViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.orbitmvi.orbit.compose.collectAsState
|
||||
|
||||
@@ -63,11 +67,24 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.current
|
||||
val isTv = LocalIsAndroidTV.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val supportState by viewModel.collectAsState()
|
||||
|
||||
val clipboardManager = rememberClipboardHelper()
|
||||
|
||||
val issuesUrl = stringResource(R.string.github_url)
|
||||
val izzyUrl = stringResource(R.string.fdroid_url)
|
||||
val telegramUrl = stringResource(R.string.telegram_url)
|
||||
val matrixUrl = stringResource(R.string.matrix_url)
|
||||
val docsUrl = stringResource(R.string.docs_url)
|
||||
val websiteUrl = stringResource(R.string.website_url)
|
||||
val translationUrl = stringResource(R.string.translation_url)
|
||||
val privacyPolicyUrl = stringResource(R.string.privacy_policy_url)
|
||||
val playStoreUrl = "https://play.google.com/store/apps/details?id=${context.packageName}"
|
||||
val playReviewsUrl =
|
||||
"https://play.google.com/store/apps/details?id=${context.packageName}&showAllReviews=true"
|
||||
|
||||
val version = remember {
|
||||
"v${BuildConfig.VERSION_NAME +
|
||||
if(BuildConfig.DEBUG) "-debug" else "" }"
|
||||
@@ -95,6 +112,19 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
PermissionDialog(context = context, onDismiss = { showPermissionDialog = false })
|
||||
}
|
||||
|
||||
fun openWebUrl(url: String) {
|
||||
context.openWebUrl(url).onFailure {
|
||||
scope.launch {
|
||||
viewModel.postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.no_browser_detected),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
@@ -115,19 +145,19 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
)
|
||||
SurfaceRow(
|
||||
stringResource(R.string.docs_description),
|
||||
onClick = { context.openWebUrl(context.getString(R.string.docs_url)) },
|
||||
onClick = { openWebUrl(docsUrl) },
|
||||
leading = { Icon(Icons.Outlined.Book, contentDescription = null) },
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
)
|
||||
SurfaceRow(
|
||||
stringResource(R.string.website),
|
||||
onClick = { context.openWebUrl(context.getString(R.string.website_url)) },
|
||||
onClick = { openWebUrl(websiteUrl) },
|
||||
leading = { Icon(Icons.Outlined.Web, contentDescription = null) },
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
)
|
||||
SurfaceRow(
|
||||
stringResource(R.string.translation),
|
||||
onClick = { context.openWebUrl(context.getString(R.string.translation_url)) },
|
||||
onClick = { openWebUrl(translationUrl) },
|
||||
description = { DescriptionText(stringResource(R.string.help_translate)) },
|
||||
leading = { Icon(Icons.Outlined.Translate, contentDescription = null) },
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
@@ -141,14 +171,16 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
leading = { Icon(Icons.Outlined.Policy, contentDescription = null) },
|
||||
title = stringResource(R.string.privacy_policy),
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
onClick = { context.openWebUrl(context.getString(R.string.privacy_policy_url)) },
|
||||
onClick = { openWebUrl(privacyPolicyUrl) },
|
||||
)
|
||||
if (BuildConfig.FLAVOR == Constants.GOOGLE_PLAY_FLAVOR) {
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.Reviews, contentDescription = null) },
|
||||
title = stringResource(R.string.review),
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
onClick = { context.launchPlayStoreReview() },
|
||||
onClick = {
|
||||
context.launchPlayStoreReview().onFailure { openWebUrl(playReviewsUrl) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -167,7 +199,7 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
},
|
||||
title = stringResource(R.string.join_matrix),
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
onClick = { context.openWebUrl(context.getString(R.string.matrix_url)) },
|
||||
onClick = { openWebUrl(matrixUrl) },
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = {
|
||||
@@ -179,7 +211,7 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
},
|
||||
title = stringResource(R.string.join_telegram),
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
onClick = { context.openWebUrl(context.getString(R.string.telegram_url)) },
|
||||
onClick = { openWebUrl(telegramUrl) },
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = {
|
||||
@@ -191,13 +223,24 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
},
|
||||
title = stringResource(R.string.open_issue),
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
onClick = { context.openWebUrl(context.getString(R.string.github_url)) },
|
||||
onClick = { openWebUrl(issuesUrl) },
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.Mail, contentDescription = null) },
|
||||
title = stringResource(R.string.email_description),
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
onClick = { context.launchSupportEmail() },
|
||||
onClick = {
|
||||
context.launchSupportEmail().onFailure {
|
||||
scope.launch {
|
||||
viewModel.postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.no_email_detected),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
Column {
|
||||
@@ -222,12 +265,21 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
leading = { Icon(Icons.Outlined.InstallMobile, contentDescription = null) },
|
||||
title = stringResource(R.string.check_for_update),
|
||||
onClick = {
|
||||
if (BuildConfig.DEBUG)
|
||||
return@SurfaceRow context.showToast(R.string.update_check_unsupported)
|
||||
if (BuildConfig.DEBUG) {
|
||||
scope.launch {
|
||||
viewModel.postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.update_check_unsupported),
|
||||
ToastType.Warning,
|
||||
)
|
||||
)
|
||||
}
|
||||
return@SurfaceRow
|
||||
}
|
||||
when (BuildConfig.FLAVOR) {
|
||||
Constants.GOOGLE_PLAY_FLAVOR -> context.launchPlayStoreListing()
|
||||
Constants.FDROID_FLAVOR ->
|
||||
context.openWebUrl(context.getString(R.string.fdroid_url))
|
||||
Constants.GOOGLE_PLAY_FLAVOR ->
|
||||
context.launchPlayStoreListing().onFailure { openWebUrl(playStoreUrl) }
|
||||
Constants.FDROID_FLAVOR -> openWebUrl(izzyUrl)
|
||||
else -> viewModel.checkForStandaloneUpdate()
|
||||
}
|
||||
},
|
||||
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support.license
|
||||
|
||||
import android.content.Context
|
||||
import com.mikepenz.aboutlibraries.Libs
|
||||
import com.mikepenz.aboutlibraries.entity.Developer
|
||||
import com.mikepenz.aboutlibraries.entity.Library
|
||||
import com.mikepenz.aboutlibraries.entity.License
|
||||
import com.mikepenz.aboutlibraries.entity.Scm
|
||||
import com.mikepenz.aboutlibraries.util.withContext
|
||||
|
||||
fun buildLibsWithAdditionalLibraries(context: Context): Libs {
|
||||
val baseLibs = Libs.Builder().withContext(context).build()
|
||||
|
||||
val cleanedBaseLibs =
|
||||
baseLibs.libraries.filterNot { library ->
|
||||
library.uniqueId.contains("com.github.topjohnwu.libsu", ignoreCase = true) ||
|
||||
library.uniqueId.contains("com.github.T8RIN.QuickieExtended", ignoreCase = true)
|
||||
}
|
||||
|
||||
val nativeLibraries =
|
||||
listOf(
|
||||
Library(
|
||||
uniqueId = "github.com.wgtunnel:amneziawg-go",
|
||||
artifactVersion = "v0.0.0-20260618075902-e1b699b2104b",
|
||||
name = "AmneziaWG Go (Fork)",
|
||||
description = "WireGuard implementation with Amnezia obfuscation",
|
||||
website = "https://wgtunnel.com",
|
||||
developers =
|
||||
listOf(
|
||||
Developer(
|
||||
name = "Zane Schepke (Fork Maintainer)",
|
||||
organisationUrl = "https://wgtunnel.com",
|
||||
),
|
||||
Developer(
|
||||
name = "Jason A. Donenfeld (Original WireGuard)",
|
||||
organisationUrl = "https://www.wireguard.com/",
|
||||
),
|
||||
Developer(
|
||||
name = "Amnezia VPN Team",
|
||||
organisationUrl = "https://amnezia.org/",
|
||||
),
|
||||
),
|
||||
organization = null,
|
||||
scm = Scm(null, null, "https://github.com/wgtunnel/amneziawg-go"),
|
||||
licenses =
|
||||
setOf(
|
||||
License(
|
||||
name = "MIT License",
|
||||
url = "https://opensource.org/licenses/MIT",
|
||||
spdxId = "MIT",
|
||||
hash = "mit-license-amneziawg-fork",
|
||||
)
|
||||
),
|
||||
funding = emptySet(),
|
||||
tag = "native",
|
||||
),
|
||||
Library(
|
||||
uniqueId = "github.com.wgtunnel:wireproxy-awg",
|
||||
artifactVersion = "v0.0.0-20260309043206-ff4200f20ff2",
|
||||
name = "Wireproxy AWG (Fork)",
|
||||
description = "WireGuard proxy with Amnezia support",
|
||||
website = "https://wgtunnel.com",
|
||||
developers =
|
||||
listOf(
|
||||
Developer(
|
||||
name = "Zane Schepke (Fork Maintainer)",
|
||||
organisationUrl = "https://wgtunnel.com",
|
||||
),
|
||||
Developer(name = "Artem Russkikh (Original)", organisationUrl = null),
|
||||
),
|
||||
organization = null,
|
||||
scm = Scm(null, null, "https://github.com/wgtunnel/wireproxy-awg"),
|
||||
licenses =
|
||||
setOf(
|
||||
License(
|
||||
name = "MIT License",
|
||||
url = "https://opensource.org/licenses/MIT",
|
||||
spdxId = "MIT",
|
||||
hash = "mit-license-wireproxy-fork",
|
||||
)
|
||||
),
|
||||
funding = emptySet(),
|
||||
tag = "native",
|
||||
),
|
||||
Library(
|
||||
uniqueId = "github.com.wgtunnel:go-socks5",
|
||||
artifactVersion = "v0.0.0-20260307052555-86f8d93b9534",
|
||||
name = "go-socks5 (Fork)",
|
||||
description = "SOCKS5 proxy server implementation",
|
||||
website = "https://wgtunnel.com",
|
||||
developers =
|
||||
listOf(
|
||||
Developer(
|
||||
name = "Zane Schepke (Fork Maintainer)",
|
||||
organisationUrl = "https://wgtunnel.com",
|
||||
),
|
||||
Developer(name = "Things-go Team (Original)", organisationUrl = null),
|
||||
),
|
||||
organization = null,
|
||||
scm = Scm(null, null, "https://github.com/wgtunnel/go-socks5"),
|
||||
licenses =
|
||||
setOf(
|
||||
License(
|
||||
name = "MIT License",
|
||||
url = "https://opensource.org/licenses/MIT",
|
||||
spdxId = "MIT",
|
||||
hash = "mit-license-go-socks5-fork",
|
||||
)
|
||||
),
|
||||
funding = emptySet(),
|
||||
tag = "native",
|
||||
),
|
||||
Library(
|
||||
uniqueId = "github.com.miekg:dns",
|
||||
artifactVersion = "v1.1.69",
|
||||
name = "miekg/dns",
|
||||
description = "DNS library for Go",
|
||||
website = "https://github.com/miekg/dns",
|
||||
developers = listOf(Developer(name = "Miek Gieben", organisationUrl = null)),
|
||||
organization = null,
|
||||
scm = Scm(null, null, "https://github.com/miekg/dns"),
|
||||
licenses =
|
||||
setOf(
|
||||
License(
|
||||
name = "BSD 3-Clause \"New\" or \"Revised\" License",
|
||||
url = "https://opensource.org/licenses/BSD-3-Clause",
|
||||
spdxId = "BSD-3-Clause",
|
||||
hash = "bsd3-miekg-dns",
|
||||
)
|
||||
),
|
||||
funding = emptySet(),
|
||||
tag = "go",
|
||||
),
|
||||
Library(
|
||||
uniqueId = "github.com.heiher:hev-socks5-tunnel",
|
||||
artifactVersion = "2.15.0",
|
||||
name = "hev-socks5-tunnel",
|
||||
description = "High performance SOCKS5 tunnel",
|
||||
website = "https://github.com/heiher/hev-socks5-tunnel",
|
||||
developers = listOf(Developer(name = "heiher", organisationUrl = null)),
|
||||
organization = null,
|
||||
scm = Scm(null, null, "https://github.com/heiher/hev-socks5-tunnel"),
|
||||
licenses =
|
||||
setOf(
|
||||
License(
|
||||
name = "MIT License",
|
||||
url = "https://opensource.org/licenses/MIT",
|
||||
spdxId = "MIT",
|
||||
hash = "mit-license-hev",
|
||||
)
|
||||
),
|
||||
funding = emptySet(),
|
||||
tag = "native",
|
||||
),
|
||||
)
|
||||
|
||||
val additionalLibraries =
|
||||
listOf(
|
||||
Library(
|
||||
uniqueId = "com.github.T8RIN.QuickieExtended:quickie-foss",
|
||||
artifactVersion = "1.18.1",
|
||||
name = "QuickieFoss",
|
||||
description = "Camera QR code scanner",
|
||||
website = "https://github.com/T8RIN/QuickieExtended",
|
||||
developers = listOf(Developer(name = "T8RIN", null)),
|
||||
organization = null,
|
||||
scm = Scm(null, null, "https://github.com/T8RIN/QuickieExtended"),
|
||||
licenses =
|
||||
setOf(
|
||||
License(
|
||||
name = "Apache License 2.0",
|
||||
url = "https://www.apache.org/licenses/LICENSE-2.0",
|
||||
spdxId = "Apache-2.0",
|
||||
hash = "apache-2-quickie",
|
||||
)
|
||||
),
|
||||
funding = emptySet(),
|
||||
tag = "ui",
|
||||
),
|
||||
Library(
|
||||
uniqueId = "com.github.topjohnwu.libsu:core",
|
||||
artifactVersion = "6.0.0",
|
||||
name = "libsu",
|
||||
description = "Root shell library for Android",
|
||||
website = "https://github.com/topjohnwu/libsu",
|
||||
developers = listOf(Developer(name = "topjohnwu", null)),
|
||||
organization = null,
|
||||
scm = Scm(null, null, "https://github.com/topjohnwu/libsu"),
|
||||
licenses =
|
||||
setOf(
|
||||
License(
|
||||
name = "Apache License 2.0",
|
||||
url = "https://www.apache.org/licenses/LICENSE-2.0",
|
||||
spdxId = "Apache-2.0",
|
||||
hash = "apache-2-libsu",
|
||||
)
|
||||
),
|
||||
funding = emptySet(),
|
||||
tag = "system",
|
||||
),
|
||||
)
|
||||
|
||||
return Libs(
|
||||
libraries =
|
||||
(cleanedBaseLibs + nativeLibraries + additionalLibraries).sortedBy {
|
||||
it.name.lowercase()
|
||||
},
|
||||
licenses =
|
||||
baseLibs.licenses +
|
||||
nativeLibraries.flatMap { it.licenses } +
|
||||
additionalLibraries.flatMap { it.licenses },
|
||||
)
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class LicenseFileEntry(
|
||||
val groupId: String,
|
||||
val artifactId: String,
|
||||
val version: String,
|
||||
val name: String? = null,
|
||||
val spdxLicenses: List<SpdxLicense> = emptyList(),
|
||||
val scm: Scm? = null,
|
||||
)
|
||||
|
||||
@Serializable data class SpdxLicense(val identifier: String, val name: String, val url: String)
|
||||
|
||||
@Serializable data class Scm(val url: String)
|
||||
+12
-19
@@ -1,30 +1,23 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support.license
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.CircularWavyProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.license.components.LicenseList
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.LicenseViewModel
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.orbitmvi.orbit.compose.collectAsState
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
||||
import com.mikepenz.aboutlibraries.ui.compose.variant.LibraryDetailMode
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun LicenseScreen(viewModel: LicenseViewModel = koinViewModel()) {
|
||||
val licenseUiState by viewModel.collectAsState()
|
||||
fun LicenseScreen() {
|
||||
val context = LocalContext.current
|
||||
val libs = remember { buildLibsWithAdditionalLibraries(context) }
|
||||
|
||||
if (licenseUiState.isLoading) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularWavyProgressIndicator(waveSpeed = 60.dp, modifier = Modifier.size(48.dp))
|
||||
}
|
||||
} else {
|
||||
LicenseList(licenseUiState.licenses)
|
||||
}
|
||||
LibrariesContainer(
|
||||
libraries = libs,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
detailMode = LibraryDetailMode.Sheet,
|
||||
)
|
||||
}
|
||||
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support.license.components
|
||||
|
||||
import LicenseFileEntry
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.Launch
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.scrollbar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||
|
||||
@Composable
|
||||
fun LicenseList(licenses: List<LicenseFileEntry>) {
|
||||
val context = LocalContext.current
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
LazyColumn(
|
||||
modifier =
|
||||
Modifier.fillMaxSize()
|
||||
.scrollbar(
|
||||
state = lazyListState.scrollIndicatorState,
|
||||
orientation = Orientation.Vertical,
|
||||
),
|
||||
state = lazyListState,
|
||||
) {
|
||||
items(licenses) { entry ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier =
|
||||
Modifier.clickable(enabled = entry.scm?.url != null) {
|
||||
entry.scm?.url?.let { context.openWebUrl(it) }
|
||||
}
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "${entry.artifactId} (${entry.version})",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
|
||||
entry.spdxLicenses.forEach { license ->
|
||||
Text(
|
||||
text = license.name,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
entry.scm?.url?.let { Icon(Icons.AutoMirrored.Outlined.Launch, null) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
-7
@@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||
@@ -50,11 +51,15 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
||||
rememberFileExportLauncherForResult(
|
||||
onSuccess = { uri -> sharedViewModel.exportSelectedTunnels(uri) },
|
||||
onCanceled = {
|
||||
sharedViewModel.showToast(StringValue.StringResource(R.string.export_canceled))
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.export_canceled),
|
||||
ToastType.Warning,
|
||||
)
|
||||
},
|
||||
onUnsupported = {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.export_unsupported)
|
||||
StringValue.StringResource(R.string.export_unsupported),
|
||||
ToastType.Warning,
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -73,7 +78,8 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
||||
selectedTunnelsExportLauncher.launch(fileName)
|
||||
} else {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.error_no_file_explorer)
|
||||
StringValue.StringResource(R.string.error_no_file_explorer),
|
||||
ToastType.Error,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -87,7 +93,8 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
||||
rememberFileImportLauncherForResult(
|
||||
onNoFileExplorer = {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.error_no_file_explorer)
|
||||
StringValue.StringResource(R.string.error_no_file_explorer),
|
||||
ToastType.Error,
|
||||
)
|
||||
},
|
||||
onData = { data -> sharedViewModel.importFromUri(data) },
|
||||
@@ -101,13 +108,15 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
||||
}
|
||||
QRResult.QRMissingPermission -> {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.camera_permission_required)
|
||||
StringValue.StringResource(R.string.camera_permission_required),
|
||||
ToastType.Warning,
|
||||
)
|
||||
}
|
||||
is QRResult.QRSuccess -> {
|
||||
result.content.rawValue?.let { sharedViewModel.importFromQr(it) }
|
||||
?: sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.config_error)
|
||||
StringValue.StringResource(R.string.config_error),
|
||||
ToastType.Error,
|
||||
)
|
||||
}
|
||||
QRResult.QRUserCanceled -> Unit
|
||||
@@ -119,7 +128,8 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
||||
->
|
||||
if (!isGranted) {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.camera_permission_required)
|
||||
StringValue.StringResource(R.string.camera_permission_required),
|
||||
ToastType.Warning,
|
||||
)
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
|
||||
+1
-1
@@ -42,6 +42,6 @@ private fun TransferMetric(icon: ImageVector, text: String, style: TextStyle, co
|
||||
modifier = Modifier.size(12.dp),
|
||||
)
|
||||
|
||||
Text(text = text.lowercase(), style = style, color = color)
|
||||
Text(text = text, style = style, color = color)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-15
@@ -17,7 +17,6 @@ import androidx.compose.material3.scrollbar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -38,8 +37,6 @@ import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.TunnelsUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
@@ -52,14 +49,6 @@ fun TunnelList(
|
||||
val context = LocalContext.current
|
||||
val isTv = LocalIsAndroidTV.current
|
||||
|
||||
val now by
|
||||
produceState(System.currentTimeMillis()) {
|
||||
while (true) {
|
||||
delay(1_000L.milliseconds)
|
||||
value = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
@@ -105,10 +94,7 @@ fun TunnelList(
|
||||
uiState.backendStatus.activeTunnels[tunnel.id] ?: ActiveTunnel()
|
||||
}
|
||||
|
||||
val displayState =
|
||||
remember(activeTunnel, now, uiState.displayStates[tunnel.id]) {
|
||||
uiState.displayStates[tunnel.id] ?: DisplayTunnelState.from(activeTunnel, now)
|
||||
}
|
||||
val displayState = remember(activeTunnel) { DisplayTunnelState.from(activeTunnel) }
|
||||
|
||||
val isRunning = uiState.backendStatus.activeTunnels.containsKey(tunnel.id)
|
||||
|
||||
|
||||
+9
-2
@@ -28,14 +28,16 @@ import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.config.components.QrCodeDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.ConfigHeaderColor
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.ConfigKeyColor
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isTextTooLargeForQr
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
|
||||
import org.koin.compose.viewmodel.koinActivityViewModel
|
||||
@@ -76,7 +78,12 @@ fun ConfigScreen(
|
||||
when (sideEffect) {
|
||||
is LocalSideEffect.Modal.QR -> {
|
||||
if (tunnel.quickConfig.isTextTooLargeForQr()) {
|
||||
context.showToast(R.string.text_too_large_for_qr)
|
||||
sharedViewModel.postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.text_too_large_for_qr),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
} else {
|
||||
showQrModal = true
|
||||
}
|
||||
|
||||
+23
-63
@@ -26,97 +26,57 @@ sealed class DisplayTunnelState {
|
||||
|
||||
data object Connected : DisplayTunnelState()
|
||||
|
||||
data object Degraded : DisplayTunnelState()
|
||||
data object HandshakeFailure : DisplayTunnelState()
|
||||
|
||||
@StringRes
|
||||
fun labelRes(): Int {
|
||||
return when (this) {
|
||||
fun labelRes(): Int =
|
||||
when (this) {
|
||||
Disconnected -> R.string.tunnel_state_disconnected
|
||||
Connecting -> R.string.tunnel_state_starting
|
||||
ResolvingDns -> R.string.tunnel_state_resolving_dns
|
||||
Connecting,
|
||||
EstablishingConnection -> R.string.tunnel_state_establishing_connection
|
||||
ResolvingDns -> R.string.tunnel_state_resolving_dns
|
||||
Ready -> R.string.ready
|
||||
Connected -> R.string.tunnel_state_connected
|
||||
Degraded -> R.string.tunnel_state_handshake_failure
|
||||
HandshakeFailure -> R.string.tunnel_state_handshake_failure
|
||||
}
|
||||
|
||||
fun asColor(): Color =
|
||||
when (this) {
|
||||
Disconnected -> CoolGray
|
||||
Connecting,
|
||||
ResolvingDns,
|
||||
EstablishingConnection,
|
||||
Ready -> Straw
|
||||
Connected -> SilverTree
|
||||
HandshakeFailure -> AlertRed
|
||||
}
|
||||
}
|
||||
|
||||
fun asLocalizedString(context: Context): String {
|
||||
return context.getString(labelRes())
|
||||
}
|
||||
|
||||
fun asColor(): Color {
|
||||
return when (this) {
|
||||
Disconnected -> CoolGray
|
||||
|
||||
Connecting,
|
||||
ResolvingDns,
|
||||
EstablishingConnection,
|
||||
Ready -> Straw
|
||||
|
||||
Connected -> SilverTree
|
||||
|
||||
Degraded -> AlertRed
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val HANDSHAKE_FAILURE_DEGRADED_THRESHOLD_MS = 6_000L
|
||||
|
||||
// During this window we avoid showing Degraded even if we see HandshakeFailure
|
||||
private const val POST_RESOLUTION_GRACE_PERIOD_MS = 3_500L
|
||||
|
||||
fun from(activeTunnel: ActiveTunnel, now: Long): DisplayTunnelState {
|
||||
fun from(activeTunnel: ActiveTunnel): DisplayTunnelState {
|
||||
val transport = activeTunnel.transportState
|
||||
val bootstrap = activeTunnel.bootstrapState
|
||||
val mode = activeTunnel.mode
|
||||
val isVpnStyle = mode is BackendMode.Vpn || mode is BackendMode.Proxy.KillSwitchPrimary
|
||||
|
||||
val bootstrapPhaseDone =
|
||||
bootstrap is BootstrapState.Complete || bootstrap is BootstrapState.None
|
||||
|
||||
// Check if we recently completed peer resolution
|
||||
val recentlyResolvedPeers =
|
||||
activeTunnel.lastPeerUpdateMs > 0 &&
|
||||
(now - activeTunnel.lastPeerUpdateMs) < POST_RESOLUTION_GRACE_PERIOD_MS
|
||||
|
||||
return when {
|
||||
transport is Tunnel.State.Down -> Disconnected
|
||||
|
||||
bootstrap is BootstrapState.Failed -> Degraded
|
||||
bootstrap is BootstrapState.Failed -> HandshakeFailure
|
||||
|
||||
bootstrap is BootstrapState.ResolvingDns ||
|
||||
bootstrap is BootstrapState.UpdatingPeers -> ResolvingDns
|
||||
|
||||
transport is Tunnel.State.Up.Healthy -> Connected
|
||||
|
||||
transport is Tunnel.State.Up.HandshakeFailure -> {
|
||||
val age = now - activeTunnel.lastStateChangeMs
|
||||
transport is Tunnel.State.Up.HandshakeFailure -> HandshakeFailure
|
||||
|
||||
if (recentlyResolvedPeers && bootstrapPhaseDone) {
|
||||
if (isVpnStyle) EstablishingConnection else Ready
|
||||
} else if (
|
||||
age > HANDSHAKE_FAILURE_DEGRADED_THRESHOLD_MS && bootstrapPhaseDone
|
||||
) {
|
||||
Degraded
|
||||
} else if (isVpnStyle && bootstrapPhaseDone) {
|
||||
EstablishingConnection
|
||||
} else if (bootstrapPhaseDone) {
|
||||
Ready
|
||||
} else {
|
||||
Connecting
|
||||
}
|
||||
}
|
||||
transport is Tunnel.State.Starting ->
|
||||
if (isVpnStyle) EstablishingConnection else Ready
|
||||
|
||||
transport is Tunnel.State.Starting -> {
|
||||
when {
|
||||
bootstrapPhaseDone -> if (isVpnStyle) EstablishingConnection else Ready
|
||||
else -> Connecting
|
||||
}
|
||||
}
|
||||
|
||||
bootstrapPhaseDone -> if (isVpnStyle) EstablishingConnection else Ready
|
||||
else -> Connecting
|
||||
else -> if (isVpnStyle) EstablishingConnection else Ready
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,5 +18,6 @@ data class GlobalAppUiState(
|
||||
val selectedTunnelCount: Int = 0,
|
||||
val alreadyDonated: Boolean = false,
|
||||
val isPinVerified: Boolean = false,
|
||||
val pendingWgImportUrl: String? = null,
|
||||
val isScreenRecordingProtectionEnabled: Boolean = false,
|
||||
)
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.state
|
||||
|
||||
import LicenseFileEntry
|
||||
|
||||
data class LicenseUiState(
|
||||
val isLoading: Boolean = true,
|
||||
val licenses: List<LicenseFileEntry> = emptyList(),
|
||||
)
|
||||
@@ -16,14 +16,14 @@ val ElectricTeal = Color(0xFF4DD0E1)
|
||||
// Status colors
|
||||
val SilverTree = Color(0xFF6DB58B)
|
||||
val AlertRed = Color(0xFFCF6679)
|
||||
|
||||
val Straw = Color(0xFFD4C483)
|
||||
|
||||
val Disabled = CoolGray.copy(alpha = 0.4f)
|
||||
|
||||
// Config colors
|
||||
// Other colors
|
||||
val ConfigHeaderColor = Color(0xFFBB86FC)
|
||||
val ConfigKeyColor = Color(0xFF03DAC5)
|
||||
val Heart = Color(0xFFDB61A2)
|
||||
|
||||
sealed class ThemeColors(
|
||||
val background: Color,
|
||||
|
||||
@@ -6,15 +6,11 @@ object Constants {
|
||||
const val BASE_LOG_FILE_NAME = "wg_tunnel_logs"
|
||||
|
||||
const val VPN_SETTINGS_PACKAGE = "android.net.vpn.SETTINGS"
|
||||
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1 shl 10
|
||||
const val SPECIAL_USE_SERVICE_TYPE_ID = 1 shl 30
|
||||
|
||||
const val QR_CODE_NAME_PROPERTY = "# Name ="
|
||||
|
||||
const val FDROID_FLAVOR = "fdroid"
|
||||
const val GOOGLE_PLAY_FLAVOR = "google"
|
||||
const val STANDALONE_FLAVOR = "standalone"
|
||||
const val RELEASE = "release"
|
||||
|
||||
const val BASE_RELEASE_URL = "https://github.com/wgtunnel/wgtunnel/releases/tag/"
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ object DnsValidator {
|
||||
return Result.Valid
|
||||
}
|
||||
|
||||
private fun validateUdp(value: String): DnsValidator.Result {
|
||||
private fun validateUdp(value: String): Result {
|
||||
val parts = value.split(":")
|
||||
|
||||
val host = parts.getOrNull(0)?.trim()
|
||||
@@ -93,14 +93,14 @@ object DnsValidator {
|
||||
|
||||
// basic IP/hostname sanity check
|
||||
if (!isValidHostOrIp(host)) {
|
||||
return DnsValidator.Result.Invalid(DnsError.InvalidIpOrHost)
|
||||
return Result.Invalid(DnsError.InvalidIpOrHost)
|
||||
}
|
||||
|
||||
if (port !in 1..65535) {
|
||||
return DnsValidator.Result.Invalid(DnsError.InvalidPort)
|
||||
return Result.Invalid(DnsError.InvalidPort)
|
||||
}
|
||||
|
||||
return DnsValidator.Result.Valid
|
||||
return Result.Valid
|
||||
}
|
||||
|
||||
private fun isValidHostOrIp(value: String): Boolean {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util
|
||||
|
||||
import LicenseFileEntry
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
@@ -23,7 +22,6 @@ import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import timber.log.Timber
|
||||
|
||||
class FileUtils(private val context: Context, private val ioDispatcher: CoroutineDispatcher) {
|
||||
@@ -293,15 +291,6 @@ class FileUtils(private val context: Context, private val ioDispatcher: Coroutin
|
||||
return@withContext itemUri
|
||||
}
|
||||
|
||||
suspend fun readLibraryLicensesFromAssets(): List<LicenseFileEntry> =
|
||||
withContext(ioDispatcher) {
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
val jsonResult =
|
||||
context.assets.open("licenses.json").bufferedReader().use { it.readText() }
|
||||
json.decodeFromString(jsonResult)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val CONF_FILE_EXTENSION = ".conf"
|
||||
const val ZIP_FILE_EXTENSION = ".zip"
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util
|
||||
|
||||
import com.vdurmont.semver4j.Semver
|
||||
import java.math.BigDecimal
|
||||
import kotlin.math.pow
|
||||
import timber.log.Timber
|
||||
|
||||
object NumberUtils {
|
||||
private const val BYTES_IN_KB = 1024.0
|
||||
private val BYTES_IN_MB = BYTES_IN_KB.pow(2.0)
|
||||
private val keyValidationRegex = """^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=${'$'}""".toRegex()
|
||||
|
||||
fun bytesToMB(bytes: Long): BigDecimal {
|
||||
return bytes.toBigDecimal().divide(BYTES_IN_MB.toBigDecimal())
|
||||
}
|
||||
|
||||
fun isValidKey(key: String): Boolean {
|
||||
return key.matches(keyValidationRegex)
|
||||
}
|
||||
|
||||
fun generateRandomTunnelName(): String {
|
||||
return "tunnel${randomFive()}"
|
||||
|
||||
+16
-109
@@ -1,27 +1,18 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util.extensions
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Context.POWER_SERVICE
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import android.service.quicksettings.TileService
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.net.toUri
|
||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.state.TunnelApp
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import java.io.File
|
||||
@@ -30,22 +21,11 @@ import java.util.Locale
|
||||
import kotlin.system.exitProcess
|
||||
import timber.log.Timber
|
||||
|
||||
fun Context.openWebUrl(url: String): Result<Unit> {
|
||||
return kotlin
|
||||
.runCatching {
|
||||
val webpage: Uri = url.toUri()
|
||||
val intent =
|
||||
Intent(Intent.ACTION_VIEW, webpage).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
.onFailure { showToast(R.string.no_browser_detected) }
|
||||
}
|
||||
|
||||
fun Context.isBatteryOptimizationsDisabled(): Boolean {
|
||||
val pm = getSystemService(POWER_SERVICE) as PowerManager
|
||||
return pm.isIgnoringBatteryOptimizations(packageName)
|
||||
fun Context.openWebUrl(url: String): Result<Unit> = runCatching {
|
||||
val webpage: Uri = url.toUri()
|
||||
val intent =
|
||||
Intent(Intent.ACTION_VIEW, webpage).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
fun Context.launchNotificationSettings() {
|
||||
@@ -94,30 +74,7 @@ fun Context.hasSAFSupport(mimeType: String): Boolean {
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.launchShareFile(file: File) {
|
||||
FileProvider.getUriForFile(this, getString(R.string.provider), file)
|
||||
val shareIntent =
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
type = FileUtils.ALL_FILE_TYPES
|
||||
putExtra(Intent.EXTRA_STREAM, file)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
val chooserIntent =
|
||||
Intent.createChooser(shareIntent, "").apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
|
||||
this.startActivity(chooserIntent)
|
||||
}
|
||||
|
||||
fun Context.showToast(resId: Int) {
|
||||
Toast.makeText(this, this.getString(resId), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
fun Context.showToast(message: String) {
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
fun Context.launchSupportEmail() {
|
||||
fun Context.launchSupportEmail(): Result<Unit> = runCatching {
|
||||
val intent =
|
||||
Intent(Intent.ACTION_SENDTO).apply {
|
||||
data = "mailto:".toUri()
|
||||
@@ -132,7 +89,7 @@ fun Context.launchSupportEmail() {
|
||||
}
|
||||
)
|
||||
} else {
|
||||
showToast(R.string.no_email_detected)
|
||||
throw IllegalStateException("No email client found")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +100,7 @@ fun Context.isRunningOnTv(): Boolean {
|
||||
fun Context.launchVpnSettings(): Result<Unit> {
|
||||
return kotlin.runCatching {
|
||||
val intent =
|
||||
Intent(Constants.VPN_SETTINGS_PACKAGE).apply { setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
|
||||
Intent(Constants.VPN_SETTINGS_PACKAGE).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK }
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
@@ -162,14 +119,6 @@ fun Context.launchLocationServicesSettings(): Result<Unit> {
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.launchSettings(): Result<Unit> {
|
||||
return kotlin.runCatching {
|
||||
val intent =
|
||||
Intent(Settings.ACTION_SETTINGS).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK }
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.launchAppSettings() {
|
||||
kotlin
|
||||
.runCatching {
|
||||
@@ -186,48 +135,6 @@ fun Context.launchAppSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.requestTunnelTileServiceStateUpdate() =
|
||||
runCatching {
|
||||
TileService.requestListeningState(
|
||||
this,
|
||||
ComponentName(this, TunnelControlTile::class.java),
|
||||
)
|
||||
}
|
||||
.onFailure { Timber.w(it) }
|
||||
|
||||
fun Context.requestAutoTunnelTileServiceUpdate() =
|
||||
runCatching {
|
||||
TileService.requestListeningState(
|
||||
this,
|
||||
ComponentName(this, AutoTunnelControlTile::class.java),
|
||||
)
|
||||
}
|
||||
.onFailure { Timber.w(it) }
|
||||
|
||||
fun Context.getAllInternetCapablePackages(): List<PackageInfo> {
|
||||
val permissions = arrayOf(Manifest.permission.INTERNET)
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
packageManager.getPackagesHoldingPermissions(
|
||||
permissions,
|
||||
PackageManager.PackageInfoFlags.of(0L),
|
||||
)
|
||||
} else {
|
||||
packageManager.getPackagesHoldingPermissions(permissions, 0)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.getSplitTunnelApps(): List<TunnelApp> {
|
||||
val packages = getAllInternetCapablePackages()
|
||||
return packages
|
||||
.filter { it.applicationInfo != null }
|
||||
.map { pkg ->
|
||||
TunnelApp(
|
||||
packageManager.getApplicationLabel(pkg.applicationInfo!!).toString(),
|
||||
pkg.packageName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.canInstallPackages(): Boolean {
|
||||
return packageManager.canRequestPackageInstalls()
|
||||
}
|
||||
@@ -242,7 +149,7 @@ fun Context.requestInstallPackagesPermission() {
|
||||
}
|
||||
|
||||
fun Context.installApk(apkFile: File) {
|
||||
val apkUri = FileProvider.getUriForFile(this, getString(R.string.provider), apkFile)
|
||||
val apkUri = FileProvider.getUriForFile(this, BuildConfig.FILE_PROVIDER_AUTHORITY, apkFile)
|
||||
val intent =
|
||||
Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(apkUri, "application/vnd.android.package-archive")
|
||||
@@ -252,9 +159,9 @@ fun Context.installApk(apkFile: File) {
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
fun Context.launchPlayStoreListing() {
|
||||
fun Context.launchPlayStoreListing(): Result<Unit> = runCatching {
|
||||
val intent =
|
||||
Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$packageName")).apply {
|
||||
Intent(Intent.ACTION_VIEW, "market://details?id=$packageName".toUri()).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
setPackage("com.android.vending")
|
||||
}
|
||||
@@ -262,12 +169,12 @@ fun Context.launchPlayStoreListing() {
|
||||
if (intent.resolveActivity(packageManager) != null) {
|
||||
startActivity(intent)
|
||||
} else {
|
||||
openWebUrl("https://play.google.com/store/apps/details?id=$packageName")
|
||||
throw IllegalStateException("Play Store not found")
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.launchPlayStoreReview() {
|
||||
val uri = Uri.parse("market://details?id=$packageName&showAllReviews=true")
|
||||
fun Context.launchPlayStoreReview(): Result<Unit> = runCatching {
|
||||
val uri = "market://details?id=$packageName&showAllReviews=true".toUri()
|
||||
val intent =
|
||||
Intent(Intent.ACTION_VIEW, uri).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
@@ -276,7 +183,7 @@ fun Context.launchPlayStoreReview() {
|
||||
if (intent.resolveActivity(packageManager) != null) {
|
||||
startActivity(intent)
|
||||
} else {
|
||||
openWebUrl("https://play.google.com/store/apps/details?id=$packageName&showAllReviews=true")
|
||||
throw IllegalStateException("Play Store not found")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util.extensions
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
fun <K, V> Flow<Map<K, V>>.distinctByKeys(): Flow<Map<K, V>> {
|
||||
return distinctUntilChanged { old, new -> old.keys == new.keys }
|
||||
}
|
||||
@@ -18,10 +18,6 @@ fun <T, R : Comparable<R>> List<T>.isSortedBy(selector: (T) -> R): Boolean {
|
||||
return zipWithNext().all { (a, b) -> selector(a) <= selector(b) }
|
||||
}
|
||||
|
||||
fun Int.toMillis(): Long {
|
||||
return this * 1_000L
|
||||
}
|
||||
|
||||
fun Double.round(decimals: Int): Double {
|
||||
val factor = 10.0.pow(decimals)
|
||||
return (this * factor).roundToInt() / factor
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util.extensions
|
||||
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
|
||||
suspend fun HttpResponse.isHtmlResponse(): Boolean {
|
||||
val contentType = headers["Content-Type"] ?: ""
|
||||
if (contentType.contains("text/html", ignoreCase = true)) return true
|
||||
|
||||
val bodyStart = bodyAsText().trimStart()
|
||||
return bodyStart.startsWith("<!DOCTYPE", ignoreCase = true) ||
|
||||
bodyStart.startsWith("<html", ignoreCase = true)
|
||||
}
|
||||
+1
-1
@@ -124,7 +124,7 @@ fun DnsError.labelRes(): Int {
|
||||
fun ActiveTunnel.statusText(context: Context): String {
|
||||
return context.getString(
|
||||
R.string.status_template,
|
||||
DisplayTunnelState.from(this, System.currentTimeMillis()).asLocalizedString(context),
|
||||
DisplayTunnelState.from(this).asLocalizedString(context),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util.permission
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
object LocalNetworkPermissionHelper {
|
||||
|
||||
fun shouldRequestPermission(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN
|
||||
}
|
||||
|
||||
fun isPermissionGranted(context: Context): Boolean {
|
||||
return if (shouldRequestPermission()) {
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_LOCAL_NETWORK) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
-10
@@ -2,13 +2,12 @@ package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
||||
import com.zaneschepke.tunnel.backend.RootShell
|
||||
import com.zaneschepke.tunnel.util.RootShell
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.WifiDetectionMethod
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
@@ -16,6 +15,8 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsR
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.AutoTunnelUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@@ -99,7 +100,10 @@ class AutoTunnelViewModel(
|
||||
val trimmed = name.trim()
|
||||
if (state.autoTunnelSettings.trustedNetworkSSIDs.contains(name)) {
|
||||
return@intent postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.error_ssid_exists))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.error_ssid_exists),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
setTrustedNetworkNames(
|
||||
@@ -141,6 +145,10 @@ class AutoTunnelViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
fun setDisabledOnCaptivePortal(enabled: Boolean) = intent {
|
||||
autoTunnelRepository.updateDisableOnCaptivePortal(enabled)
|
||||
}
|
||||
|
||||
fun removeTunnelNetwork(tunnel: TunnelConfig, ssid: String) = intent {
|
||||
tunnelsRepository.save(
|
||||
tunnel.copy(
|
||||
@@ -153,11 +161,19 @@ class AutoTunnelViewModel(
|
||||
when (method) {
|
||||
WifiDetectionMethod.ROOT -> {
|
||||
val accepted = RootShell.requestRootPermission()
|
||||
val message =
|
||||
if (!accepted) StringValue.StringResource(R.string.error_root_denied)
|
||||
else StringValue.StringResource(R.string.root_accepted)
|
||||
postSideEffect(GlobalSideEffect.Snackbar(message))
|
||||
if (!accepted) return@intent
|
||||
if (!accepted)
|
||||
return@intent postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.error_root_denied),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.root_accepted),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
}
|
||||
WifiDetectionMethod.SHIZUKU -> {
|
||||
requestShizuku()
|
||||
@@ -188,7 +204,10 @@ class AutoTunnelViewModel(
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.shizuku_not_detected))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.shizuku_not_detected),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+13
-5
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.MimicMode
|
||||
@@ -105,7 +106,10 @@ class ConfigEditViewModel(
|
||||
if (state.isTunnelNameTaken) {
|
||||
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.tunnel_name_taken))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.tunnel_name_taken),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
|
||||
return@intent
|
||||
@@ -113,7 +117,10 @@ class ConfigEditViewModel(
|
||||
|
||||
if (state.draft.tunnelName.isBlank()) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.name_error_empty))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.name_error_empty),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
return@intent
|
||||
}
|
||||
@@ -147,8 +154,9 @@ class ConfigEditViewModel(
|
||||
}
|
||||
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(
|
||||
StringValue.StringResource(R.string.config_changes_saved)
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.config_changes_saved),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -164,7 +172,7 @@ class ConfigEditViewModel(
|
||||
else -> StringValue.StringResource(R.string.unknown_error)
|
||||
}
|
||||
|
||||
postSideEffect(GlobalSideEffect.Snackbar(message))
|
||||
postSideEffect(GlobalSideEffect.Snackbar(message, ToastType.Error))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.DnsSettingsCoordinator
|
||||
@@ -9,7 +10,6 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsReposito
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarType
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.DnsUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.DnsValidator
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
@@ -70,7 +70,7 @@ class DnsViewModel(
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(result.error.labelRes()),
|
||||
type = SnackbarType.WARNING,
|
||||
type = ToastType.Error,
|
||||
)
|
||||
)
|
||||
return@intent
|
||||
@@ -87,7 +87,10 @@ class DnsViewModel(
|
||||
|
||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(StringValue.StringResource(R.string.config_changes_saved))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.config_changes_saved),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.LicenseUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import org.orbitmvi.orbit.ContainerHost
|
||||
import org.orbitmvi.orbit.viewmodel.container
|
||||
|
||||
class LicenseViewModel(private val fileUtils: FileUtils) :
|
||||
ContainerHost<LicenseUiState, Nothing>, ViewModel() {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val container =
|
||||
container<LicenseUiState, Nothing>(
|
||||
LicenseUiState(),
|
||||
buildSettings = { repeatOnSubscribedStopTimeout = 5000L },
|
||||
) {
|
||||
intent {
|
||||
val licenses = fileUtils.readLibraryLicensesFromAssets()
|
||||
reduce { state.copy(isLoading = false, licenses = licenses) }
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
@@ -52,7 +53,10 @@ class LockdownViewModel(
|
||||
|
||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(StringValue.StringResource(R.string.config_changes_saved))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.config_changes_saved),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.logcatter.LogReader
|
||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
@@ -61,7 +62,10 @@ class LoggerViewModel(
|
||||
fun exportLogs(uri: Uri?) = intent {
|
||||
if (uri == null) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(StringValue.StringResource(R.string.export_unsupported))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.export_unsupported),
|
||||
ToastType.Warning,
|
||||
)
|
||||
)
|
||||
return@intent
|
||||
}
|
||||
@@ -76,11 +80,12 @@ class LoggerViewModel(
|
||||
Timber.e(action)
|
||||
intent {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(
|
||||
R.string.export_failed,
|
||||
": ${action.localizedMessage}",
|
||||
)
|
||||
),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
+11
-3
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
@@ -79,7 +80,8 @@ class ProxySettingsViewModel(
|
||||
if (socksPort == null || httpPort == null || socksPort == httpPort) {
|
||||
return@intent postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.ports_must_differ)
|
||||
StringValue.StringResource(R.string.ports_must_differ),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -96,7 +98,10 @@ class ProxySettingsViewModel(
|
||||
|
||||
if (updated.proxyPassword?.any { it.isWhitespace() } == true) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.password_no_spaces))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.password_no_spaces),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
return@intent reduce { state.copy(isPasswordError = true) }
|
||||
}
|
||||
@@ -104,7 +109,10 @@ class ProxySettingsViewModel(
|
||||
proxySettingsRepository.upsert(updated)
|
||||
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.config_changes_saved))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.config_changes_saved),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||
}
|
||||
|
||||
+21
-6
@@ -1,8 +1,10 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.zaneschepke.tunnel.backend.RootShell
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.tunnel.util.RootShell
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelBackendCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
@@ -26,6 +28,7 @@ class SettingsViewModel(
|
||||
private val monitoringRepository: MonitoringSettingsRepository,
|
||||
private val globalEffectRepository: GlobalEffectRepository,
|
||||
private val tunnelCoordinator: TunnelCoordinator,
|
||||
private val tunnelBackendCoordinator: TunnelBackendCoordinator,
|
||||
) : ContainerHost<SettingUiState, Nothing>, ViewModel() {
|
||||
|
||||
override val container =
|
||||
@@ -96,15 +99,27 @@ class SettingsViewModel(
|
||||
fun setTunnelScriptedEnabled(to: Boolean) = intent {
|
||||
if (to) {
|
||||
val accepted = RootShell.requestRootPermission()
|
||||
val message =
|
||||
if (!accepted) StringValue.StringResource(R.string.error_root_denied)
|
||||
else StringValue.StringResource(R.string.root_accepted)
|
||||
postSideEffect(GlobalSideEffect.Snackbar(message))
|
||||
if (!accepted) return@intent
|
||||
if (!accepted)
|
||||
return@intent postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.error_root_denied),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.root_accepted),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
}
|
||||
settingsRepository.upsert(state.settings.copy(tunnelScriptingEnabled = to))
|
||||
}
|
||||
|
||||
fun setSeamlessNetworkRoaming(enabled: Boolean) = intent {
|
||||
tunnelBackendCoordinator.changeSeamlessRoaming(enabled)
|
||||
}
|
||||
|
||||
fun setAlreadyDonated(to: Boolean) = intent {
|
||||
settingsRepository.upsert(state.settings.copy(alreadyDonated = to))
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user