mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 34d71a6096 |
@@ -70,7 +70,7 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
# fix hardcode changelog file name
|
# fix hardcode changelog file name
|
||||||
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/33200.txt
|
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/32300.txt
|
||||||
tag_name: ${{ github.ref_name }}
|
tag_name: ${{ github.ref_name }}
|
||||||
name: Release ${{ github.ref_name }}
|
name: Release ${{ github.ref_name }}
|
||||||
draft: false
|
draft: false
|
||||||
|
|||||||
+31
-53
@@ -23,10 +23,6 @@ android {
|
|||||||
arg("room.schemaLocation", "$projectDir/schemas")
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room
|
|
||||||
}
|
|
||||||
|
|
||||||
resourceConfigurations.addAll(listOf("en"))
|
resourceConfigurations.addAll(listOf("en"))
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
@@ -37,51 +33,33 @@ android {
|
|||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
create(Constants.RELEASE) {
|
create(Constants.RELEASE) {
|
||||||
val properties =
|
val properties = Properties().apply {
|
||||||
Properties().apply {
|
//created local file for signing details
|
||||||
// created local file for signing details
|
try {
|
||||||
try {
|
load(file("signing.properties").reader())
|
||||||
load(file("signing.properties").reader())
|
} catch (_ : Exception) {
|
||||||
} catch (_: Exception) {
|
load(file("signing_template.properties").reader())
|
||||||
load(file("signing_template.properties").reader())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// try to get secrets from env first for pipeline build, then properties file for local build
|
//try to get secrets from env first for pipeline build, then properties file for local build
|
||||||
storeFile = file(
|
storeFile = file(System.getenv().getOrDefault(Constants.KEY_STORE_PATH_VAR, properties.getProperty(Constants.KEY_STORE_PATH_VAR)))
|
||||||
System.getenv().getOrDefault(
|
storePassword = System.getenv().getOrDefault(Constants.STORE_PASS_VAR, properties.getProperty(Constants.STORE_PASS_VAR))
|
||||||
Constants.KEY_STORE_PATH_VAR,
|
keyAlias = System.getenv().getOrDefault(Constants.KEY_ALIAS_VAR, properties.getProperty(Constants.KEY_ALIAS_VAR))
|
||||||
properties.getProperty(Constants.KEY_STORE_PATH_VAR)
|
keyPassword = System.getenv().getOrDefault(Constants.KEY_PASS_VAR, properties.getProperty(Constants.KEY_PASS_VAR))
|
||||||
)
|
|
||||||
)
|
|
||||||
storePassword = System.getenv().getOrDefault(
|
|
||||||
Constants.STORE_PASS_VAR,
|
|
||||||
properties.getProperty(Constants.STORE_PASS_VAR)
|
|
||||||
)
|
|
||||||
keyAlias = System.getenv().getOrDefault(
|
|
||||||
Constants.KEY_ALIAS_VAR,
|
|
||||||
properties.getProperty(Constants.KEY_ALIAS_VAR)
|
|
||||||
)
|
|
||||||
keyPassword = System.getenv().getOrDefault(
|
|
||||||
Constants.KEY_PASS_VAR,
|
|
||||||
properties.getProperty(Constants.KEY_PASS_VAR)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
// don't strip
|
//don't strip
|
||||||
packaging.jniLibs.keepDebugSymbols.addAll(
|
packaging.jniLibs.keepDebugSymbols.addAll(listOf("libwg-go.so", "libwg-quick.so", "libwg.so"))
|
||||||
listOf("libwg-go.so", "libwg-quick.so", "libwg.so")
|
|
||||||
)
|
|
||||||
|
|
||||||
applicationVariants.all {
|
applicationVariants.all {
|
||||||
val variant = this
|
val variant = this
|
||||||
variant.outputs
|
variant.outputs
|
||||||
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
|
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
|
||||||
.forEach { output ->
|
.forEach { output ->
|
||||||
val outputFileName =
|
val outputFileName = "${Constants.APP_NAME}-${variant.flavorName}-${variant.buildType.name}-${variant.versionName}.apk"
|
||||||
"${Constants.APP_NAME}-${variant.flavorName}-${variant.buildType.name}-${variant.versionName}.apk"
|
|
||||||
output.outputFileName = outputFileName
|
output.outputFileName = outputFileName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,7 +85,8 @@ android {
|
|||||||
}
|
}
|
||||||
create("general") {
|
create("general") {
|
||||||
dimension = Constants.TYPE
|
dimension = Constants.TYPE
|
||||||
if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle)) {
|
if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle))
|
||||||
|
{
|
||||||
apply(plugin = "com.google.gms.google-services")
|
apply(plugin = "com.google.gms.google-services")
|
||||||
apply(plugin = "com.google.firebase.crashlytics")
|
apply(plugin = "com.google.firebase.crashlytics")
|
||||||
}
|
}
|
||||||
@@ -124,6 +103,7 @@ android {
|
|||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
|
|
||||||
}
|
}
|
||||||
composeOptions {
|
composeOptions {
|
||||||
kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
|
kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
|
||||||
@@ -149,22 +129,20 @@ dependencies {
|
|||||||
implementation(libs.androidx.material3)
|
implementation(libs.androidx.material3)
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
|
|
||||||
// test
|
//test
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
testImplementation(libs.androidx.junit)
|
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||||
androidTestImplementation(libs.androidx.compose.ui.test)
|
androidTestImplementation(libs.androidx.compose.ui.test)
|
||||||
androidTestImplementation(libs.androidx.room.testing)
|
|
||||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||||
debugImplementation(libs.androidx.compose.manifest)
|
debugImplementation(libs.androidx.compose.manifest)
|
||||||
|
|
||||||
// wg
|
//wg
|
||||||
implementation(libs.tunnel)
|
implementation(libs.tunnel)
|
||||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||||
|
|
||||||
// logging
|
//logging
|
||||||
implementation(libs.timber)
|
implementation(libs.timber)
|
||||||
|
|
||||||
// compose navigation
|
// compose navigation
|
||||||
@@ -175,41 +153,41 @@ dependencies {
|
|||||||
implementation(libs.hilt.android)
|
implementation(libs.hilt.android)
|
||||||
ksp(libs.hilt.android.compiler)
|
ksp(libs.hilt.android.compiler)
|
||||||
|
|
||||||
// accompanist
|
//accompanist
|
||||||
implementation(libs.accompanist.systemuicontroller)
|
implementation(libs.accompanist.systemuicontroller)
|
||||||
implementation(libs.accompanist.permissions)
|
implementation(libs.accompanist.permissions)
|
||||||
implementation(libs.accompanist.flowlayout)
|
implementation(libs.accompanist.flowlayout)
|
||||||
implementation(libs.accompanist.drawablepainter)
|
implementation(libs.accompanist.drawablepainter)
|
||||||
|
|
||||||
// storage
|
//room
|
||||||
implementation(libs.androidx.room.runtime)
|
implementation(libs.androidx.room.runtime)
|
||||||
ksp(libs.androidx.room.compiler)
|
ksp(libs.androidx.room.compiler)
|
||||||
implementation(libs.androidx.room.ktx)
|
implementation(libs.androidx.room.ktx)
|
||||||
implementation(libs.androidx.datastore.preferences)
|
|
||||||
|
|
||||||
// lifecycle
|
//lifecycle
|
||||||
implementation(libs.lifecycle.runtime.compose)
|
implementation(libs.lifecycle.runtime.compose)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.lifecycle.process)
|
implementation(libs.androidx.lifecycle.process)
|
||||||
|
|
||||||
// icons
|
|
||||||
|
//icons
|
||||||
implementation(libs.material.icons.extended)
|
implementation(libs.material.icons.extended)
|
||||||
// serialization
|
//serialization
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
|
||||||
// firebase crashlytics
|
//firebase crashlytics
|
||||||
generalImplementation(platform(libs.firebase.bom))
|
generalImplementation(platform(libs.firebase.bom))
|
||||||
generalImplementation(libs.google.firebase.crashlytics.ktx)
|
generalImplementation(libs.google.firebase.crashlytics.ktx)
|
||||||
generalImplementation(libs.google.firebase.analytics.ktx)
|
generalImplementation(libs.google.firebase.analytics.ktx)
|
||||||
|
|
||||||
// barcode scanning
|
//barcode scanning
|
||||||
implementation(libs.zxing.android.embedded)
|
implementation(libs.zxing.android.embedded)
|
||||||
implementation(libs.zxing.core)
|
implementation(libs.zxing.core)
|
||||||
|
|
||||||
// bio
|
//bio
|
||||||
implementation(libs.androidx.biometric.ktx)
|
implementation(libs.androidx.biometric.ktx)
|
||||||
|
|
||||||
// shortcuts
|
//shortcuts
|
||||||
implementation(libs.androidx.core)
|
implementation(libs.androidx.core)
|
||||||
implementation(libs.androidx.core.google.shortcuts)
|
implementation(libs.androidx.core.google.shortcuts)
|
||||||
}
|
}
|
||||||
@@ -1,5 +1 @@
|
|||||||
-dontwarn com.google.errorprone.annotations.**
|
-dontwarn com.google.errorprone.annotations.**
|
||||||
|
|
||||||
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
|
||||||
<fields>;
|
|
||||||
}
|
|
||||||
|
|||||||
Vendored
-5
@@ -19,8 +19,3 @@
|
|||||||
# If you keep the line number information, uncomment this to
|
# If you keep the line number information, uncomment this to
|
||||||
# hide the original source file name.
|
# hide the original source file name.
|
||||||
#-renamesourcefileattribute SourceFile
|
#-renamesourcefileattribute SourceFile
|
||||||
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
|
||||||
<fields>;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,154 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 4,
|
|
||||||
"identityHash": "aee55639422df8dadfe74c3bad204477",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "Settings",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `default_tunnel` TEXT, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_battery_saver_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isAutoTunnelEnabled",
|
|
||||||
"columnName": "is_tunnel_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
|
||||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "trustedNetworkSSIDs",
|
|
||||||
"columnName": "trusted_network_ssids",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "defaultTunnel",
|
|
||||||
"columnName": "default_tunnel",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
|
||||||
"columnName": "is_always_on_vpn_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
|
||||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isShortcutsEnabled",
|
|
||||||
"columnName": "is_shortcuts_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isBatterySaverEnabled",
|
|
||||||
"columnName": "is_battery_saver_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isTunnelOnWifiEnabled",
|
|
||||||
"columnName": "is_tunnel_on_wifi_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isKernelEnabled",
|
|
||||||
"columnName": "is_kernel_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isRestoreOnBootEnabled",
|
|
||||||
"columnName": "is_restore_on_boot_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isMultiTunnelEnabled",
|
|
||||||
"columnName": "is_multi_tunnel_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "TunnelConfig",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "wgQuick",
|
|
||||||
"columnName": "wg_quick",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_TunnelConfig_name",
|
|
||||||
"unique": true,
|
|
||||||
"columnNames": [
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"views": [],
|
|
||||||
"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, 'aee55639422df8dadfe74c3bad204477')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 5,
|
|
||||||
"identityHash": "bc15003a44746e18b9c260ec49737089",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "Settings",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `default_tunnel` TEXT, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_battery_saver_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isAutoTunnelEnabled",
|
|
||||||
"columnName": "is_tunnel_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
|
||||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "trustedNetworkSSIDs",
|
|
||||||
"columnName": "trusted_network_ssids",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "defaultTunnel",
|
|
||||||
"columnName": "default_tunnel",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
|
||||||
"columnName": "is_always_on_vpn_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
|
||||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isShortcutsEnabled",
|
|
||||||
"columnName": "is_shortcuts_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isBatterySaverEnabled",
|
|
||||||
"columnName": "is_battery_saver_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isTunnelOnWifiEnabled",
|
|
||||||
"columnName": "is_tunnel_on_wifi_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isKernelEnabled",
|
|
||||||
"columnName": "is_kernel_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isRestoreOnBootEnabled",
|
|
||||||
"columnName": "is_restore_on_boot_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isMultiTunnelEnabled",
|
|
||||||
"columnName": "is_multi_tunnel_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isAutoTunnelPaused",
|
|
||||||
"columnName": "is_auto_tunnel_paused",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "TunnelConfig",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "wgQuick",
|
|
||||||
"columnName": "wg_quick",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_TunnelConfig_name",
|
|
||||||
"unique": true,
|
|
||||||
"columnNames": [
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"views": [],
|
|
||||||
"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, 'bc15003a44746e18b9c260ec49737089')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel
|
|
||||||
|
|
||||||
import androidx.room.testing.MigrationTestHelper
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class MigrationTest {
|
|
||||||
private val dbName = "migration-test"
|
|
||||||
|
|
||||||
@get:Rule
|
|
||||||
val helper: MigrationTestHelper = MigrationTestHelper(
|
|
||||||
InstrumentationRegistry.getInstrumentation(),
|
|
||||||
AppDatabase::class.java
|
|
||||||
)
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun migrate4To5() {
|
|
||||||
helper.createDatabase(dbName, 4).apply {
|
|
||||||
// Database has schema version 1. Insert some data using SQL queries.
|
|
||||||
// You can't use DAO classes because they expect the latest schema.
|
|
||||||
execSQL(
|
|
||||||
"INSERT INTO Settings (is_tunnel_enabled," +
|
|
||||||
"is_tunnel_on_mobile_data_enabled," +
|
|
||||||
"trusted_network_ssids," +
|
|
||||||
"default_tunnel," +
|
|
||||||
"is_always_on_vpn_enabled," +
|
|
||||||
"is_tunnel_on_ethernet_enabled," +
|
|
||||||
"is_shortcuts_enabled," +
|
|
||||||
"is_battery_saver_enabled," +
|
|
||||||
"is_tunnel_on_wifi_enabled," +
|
|
||||||
"is_kernel_enabled," +
|
|
||||||
"is_restore_on_boot_enabled," +
|
|
||||||
"is_multi_tunnel_enabled)" +
|
|
||||||
" VALUES " +
|
|
||||||
"('false'," +
|
|
||||||
"'false'," +
|
|
||||||
"'[trustedSSID1,trustedSSID2]'," +
|
|
||||||
"'defaultTunnel'," +
|
|
||||||
"'false'," +
|
|
||||||
"'false'," +
|
|
||||||
"'false'," +
|
|
||||||
"'false'," +
|
|
||||||
"'false'," +
|
|
||||||
"'false'," +
|
|
||||||
"'false'," +
|
|
||||||
"'false')"
|
|
||||||
)
|
|
||||||
execSQL(
|
|
||||||
"INSERT INTO TunnelConfig (name, wg_quick)" +
|
|
||||||
" VALUES ('hello', 'hello')"
|
|
||||||
)
|
|
||||||
// Prepare for the next version.
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-open the database with version 2 and provide
|
|
||||||
// MIGRATION_1_2 as the migration process.
|
|
||||||
helper.runMigrationsAndValidate(dbName, 5, true)
|
|
||||||
// MigrationTestHelper automatically verifies the schema changes,
|
|
||||||
// but you need to validate that the data was migrated properly.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,11 +17,8 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
|
||||||
<!--foreground service exempt android 14-->
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING"/>
|
||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
|
|
||||||
|
|
||||||
<!--foreground service permissions-->
|
<!--foreground service permissions-->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
@@ -45,7 +42,6 @@
|
|||||||
</queries>
|
</queries>
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:enableOnBackInvokedCallback="true"
|
|
||||||
android:name=".WireGuardAutoTunnel"
|
android:name=".WireGuardAutoTunnel"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
@@ -55,7 +51,7 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.WireguardAutoTunnel"
|
android:theme="@style/Theme.WireguardAutoTunnel"
|
||||||
tools:targetApi="tiramisu">
|
tools:targetApi="31">
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.MainActivity"
|
android:name=".ui.MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -84,8 +80,7 @@
|
|||||||
<service
|
<service
|
||||||
android:name=".service.foreground.ForegroundService"
|
android:name=".service.foreground.ForegroundService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:foregroundServiceType="systemExempted|specialUse"
|
android:foregroundServiceType="remoteMessaging"
|
||||||
tools:node="merge"
|
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
@@ -107,8 +102,7 @@
|
|||||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:persistent="true"
|
android:persistent="true"
|
||||||
android:foregroundServiceType="systemExempted|specialUse"
|
android:foregroundServiceType="remoteMessaging"
|
||||||
tools:node="merge"
|
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.net.VpnService"/>
|
<action android:name="android.net.VpnService"/>
|
||||||
@@ -121,18 +115,14 @@
|
|||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:stopWithTask="false"
|
android:stopWithTask="false"
|
||||||
android:persistent="true"
|
android:persistent="true"
|
||||||
android:foregroundServiceType="systemExempted|specialUse"
|
android:foregroundServiceType="location"
|
||||||
tools:node="merge"
|
android:permission=""
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
</service>
|
</service>
|
||||||
<receiver android:enabled="true" android:name=".receiver.BootReceiver"
|
<receiver android:enabled="true" android:name=".receiver.BootReceiver"
|
||||||
android:exported="false">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
|
||||||
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
|
|
||||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
|
||||||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver android:exported="false" android:name=".receiver.NotificationActionReceiver"/>
|
<receiver android:exported="false" android:name=".receiver.NotificationActionReceiver"/>
|
||||||
|
|||||||
+7
-9
@@ -1,12 +1,14 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.util
|
package com.zaneschepke.wireguardautotunnel
|
||||||
|
|
||||||
object Constants {
|
object Constants {
|
||||||
const val MANUAL_TUNNEL_CONFIG_ID = "0"
|
const val MANUAL_TUNNEL_CONFIG_ID = "0"
|
||||||
const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1000L // 10 minutes
|
const val WATCHER_SERVICE_WAKE_LOCK_TIMEOUT = 10*60*1000L /*10 minute*/
|
||||||
const val DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT = 30 * 60 * 1000L // 30 minutes
|
const val VPN_CONNECTIVITY_CHECK_INTERVAL = 3000L
|
||||||
const val VPN_STATISTIC_CHECK_INTERVAL = 1000L
|
const val VPN_STATISTIC_CHECK_INTERVAL = 1000L
|
||||||
const val VPN_CONNECTED_NOTIFICATION_DELAY = 3000L
|
const val TOGGLE_TUNNEL_DELAY = 500L
|
||||||
const val TOGGLE_TUNNEL_DELAY = 300L
|
const val FADE_IN_ANIMATION_DURATION = 1000
|
||||||
|
const val SLIDE_IN_ANIMATION_DURATION = 500
|
||||||
|
const val SLIDE_IN_TRANSITION_OFFSET = 1000
|
||||||
const val CONF_FILE_EXTENSION = ".conf"
|
const val CONF_FILE_EXTENSION = ".conf"
|
||||||
const val ZIP_FILE_EXTENSION = ".zip"
|
const val ZIP_FILE_EXTENSION = ".zip"
|
||||||
const val URI_CONTENT_SCHEME = "content"
|
const val URI_CONTENT_SCHEME = "content"
|
||||||
@@ -15,8 +17,4 @@ object Constants {
|
|||||||
const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs"
|
const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs"
|
||||||
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
||||||
const val EMAIL_MIME_TYPE = "message/rfc822"
|
const val EMAIL_MIME_TYPE = "message/rfc822"
|
||||||
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024
|
|
||||||
|
|
||||||
const val SUBSCRIPTION_TIMEOUT = 5_000L
|
|
||||||
const val FOCUS_REQUEST_DELAY = 500L
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import java.text.DecimalFormat
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
|
fun BroadcastReceiver.goAsync(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
block: suspend CoroutineScope.() -> Unit
|
||||||
|
) {
|
||||||
|
val pendingResult = goAsync()
|
||||||
|
@OptIn(DelicateCoroutinesApi::class) // Must run globally; there's no teardown callback.
|
||||||
|
GlobalScope.launch(context) {
|
||||||
|
try {
|
||||||
|
block()
|
||||||
|
} finally {
|
||||||
|
pendingResult.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BigDecimal.toThreeDecimalPlaceString() : String {
|
||||||
|
val df = DecimalFormat("#.###")
|
||||||
|
return df.format(this)
|
||||||
|
}
|
||||||
@@ -1,64 +1,46 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel
|
package com.zaneschepke.wireguardautotunnel
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.ComponentName
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.service.quicksettings.TileService
|
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.IOException
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class WireGuardAutoTunnel : Application() {
|
class WireGuardAutoTunnel : Application() {
|
||||||
@Inject
|
|
||||||
lateinit var settingsRepository: SettingsRepository
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var dataStoreManager: DataStoreManager
|
lateinit var settingsRepo : SettingsDoa
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
instance = this
|
if(BuildConfig.DEBUG) {
|
||||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
Timber.plant(Timber.DebugTree())
|
||||||
initSettings()
|
|
||||||
with(ProcessLifecycleOwner.get()) {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
try {
|
|
||||||
// load preferences into memory
|
|
||||||
dataStoreManager.init()
|
|
||||||
requestTileServiceStateUpdate()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Timber.e("Failed to load preferences")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
initSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initSettings() {
|
private fun initSettings() {
|
||||||
with(ProcessLifecycleOwner.get()) {
|
with(ProcessLifecycleOwner.get()) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
if (settingsRepository.getAll().isEmpty()) {
|
if(settingsRepo.getAll().isEmpty()) {
|
||||||
settingsRepository.save(Settings())
|
settingsRepo.save(Settings())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
lateinit var instance: WireGuardAutoTunnel private set
|
fun isRunningOnAndroidTv(context : Context) : Boolean {
|
||||||
fun isRunningOnAndroidTv(): Boolean {
|
return context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||||
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
|
||||||
}
|
|
||||||
fun requestTileServiceStateUpdate() {
|
|
||||||
TileService.requestListeningState(instance, ComponentName(instance, TunnelControlTile::class.java))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data
|
|
||||||
|
|
||||||
import androidx.room.AutoMigration
|
|
||||||
import androidx.room.Database
|
|
||||||
import androidx.room.RoomDatabase
|
|
||||||
import androidx.room.TypeConverters
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
|
||||||
|
|
||||||
@Database(
|
|
||||||
entities = [Settings::class, TunnelConfig::class],
|
|
||||||
version = 5,
|
|
||||||
autoMigrations = [
|
|
||||||
AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3), AutoMigration(
|
|
||||||
from = 3,
|
|
||||||
to = 4
|
|
||||||
),AutoMigration(
|
|
||||||
from = 4,
|
|
||||||
to = 5
|
|
||||||
)
|
|
||||||
],
|
|
||||||
exportSchema = true
|
|
||||||
)
|
|
||||||
@TypeConverters(DatabaseListConverters::class)
|
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
|
||||||
abstract fun settingDao(): SettingsDao
|
|
||||||
|
|
||||||
abstract fun tunnelConfigDoa(): TunnelConfigDao
|
|
||||||
}
|
|
||||||
-39
@@ -1,39 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.datastore
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.datastore.preferences.core.Preferences
|
|
||||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
|
||||||
import androidx.datastore.preferences.core.edit
|
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
|
|
||||||
class DataStoreManager(private val context: Context) {
|
|
||||||
companion object {
|
|
||||||
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
|
|
||||||
}
|
|
||||||
|
|
||||||
// preferences
|
|
||||||
private val preferencesKey = "preferences"
|
|
||||||
private val Context.dataStore by preferencesDataStore(
|
|
||||||
name = preferencesKey
|
|
||||||
)
|
|
||||||
|
|
||||||
suspend fun init() {
|
|
||||||
context.dataStore.data.first()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) =
|
|
||||||
context.dataStore.edit {
|
|
||||||
it[key] = value
|
|
||||||
}
|
|
||||||
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map {
|
|
||||||
it[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun <T> getFromStore(key: Preferences.Key<T>) = context.dataStore.data.first { it.contains(key) }[key]
|
|
||||||
|
|
||||||
val locationDisclosureFlow: Flow<Boolean?> = context.dataStore.data.map {
|
|
||||||
it[LOCATION_DISCLOSURE_SHOWN]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.model
|
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
|
||||||
import androidx.room.Entity
|
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
data class Settings(
|
|
||||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
|
||||||
@ColumnInfo(name = "is_tunnel_enabled") var isAutoTunnelEnabled: Boolean = false,
|
|
||||||
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled") var isTunnelOnMobileDataEnabled: Boolean = false,
|
|
||||||
@ColumnInfo(name = "trusted_network_ssids") var trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
|
|
||||||
@ColumnInfo(name = "default_tunnel") var defaultTunnel: String? = null,
|
|
||||||
@ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled: Boolean = false,
|
|
||||||
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled") var isTunnelOnEthernetEnabled: Boolean = false,
|
|
||||||
@ColumnInfo(
|
|
||||||
name = "is_shortcuts_enabled",
|
|
||||||
defaultValue = "false"
|
|
||||||
) var isShortcutsEnabled: Boolean = false,
|
|
||||||
@ColumnInfo(
|
|
||||||
name = "is_battery_saver_enabled",
|
|
||||||
defaultValue = "false"
|
|
||||||
) var isBatterySaverEnabled: Boolean = false,
|
|
||||||
@ColumnInfo(
|
|
||||||
name = "is_tunnel_on_wifi_enabled",
|
|
||||||
defaultValue = "false"
|
|
||||||
) var isTunnelOnWifiEnabled: Boolean = false,
|
|
||||||
@ColumnInfo(
|
|
||||||
name = "is_kernel_enabled",
|
|
||||||
defaultValue = "false"
|
|
||||||
) var isKernelEnabled: Boolean = false,
|
|
||||||
@ColumnInfo(
|
|
||||||
name = "is_restore_on_boot_enabled",
|
|
||||||
defaultValue = "false"
|
|
||||||
) var isRestoreOnBootEnabled: Boolean = false,
|
|
||||||
@ColumnInfo(
|
|
||||||
name = "is_multi_tunnel_enabled",
|
|
||||||
defaultValue = "false"
|
|
||||||
) var isMultiTunnelEnabled: Boolean = false,
|
|
||||||
@ColumnInfo(
|
|
||||||
name = "is_auto_tunnel_paused",
|
|
||||||
defaultValue = "false"
|
|
||||||
) var isAutoTunnelPaused: Boolean = false,
|
|
||||||
) {
|
|
||||||
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean {
|
|
||||||
return if (defaultTunnel != null) {
|
|
||||||
val defaultConfig = TunnelConfig.from(defaultTunnel!!)
|
|
||||||
(tunnelConfig.id == defaultConfig.id)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
interface SettingsRepository {
|
|
||||||
suspend fun save(settings : Settings)
|
|
||||||
fun getSettingsFlow() : Flow<Settings>
|
|
||||||
|
|
||||||
suspend fun getSettings() : Settings
|
|
||||||
suspend fun getAll() : List<Settings>
|
|
||||||
}
|
|
||||||
-24
@@ -1,24 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
class SettingsRepositoryImpl(private val settingsDoa: SettingsDao) : SettingsRepository {
|
|
||||||
|
|
||||||
override suspend fun save(settings: Settings) {
|
|
||||||
settingsDoa.save(settings)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getSettingsFlow(): Flow<Settings> {
|
|
||||||
return settingsDoa.getSettingsFlow()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getSettings(): Settings {
|
|
||||||
return settingsDoa.getAll().firstOrNull() ?: Settings()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getAll(): List<Settings> {
|
|
||||||
return settingsDoa.getAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-14
@@ -1,14 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
interface TunnelConfigRepository {
|
|
||||||
|
|
||||||
fun getTunnelConfigsFlow() : Flow<TunnelConfigs>
|
|
||||||
suspend fun getAll() : TunnelConfigs
|
|
||||||
suspend fun save(tunnelConfig: TunnelConfig)
|
|
||||||
suspend fun delete(tunnelConfig: TunnelConfig)
|
|
||||||
suspend fun count() : Int
|
|
||||||
}
|
|
||||||
-28
@@ -1,28 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
class TunnelConfigRepositoryImpl(private val tunnelConfigDao: TunnelConfigDao) : TunnelConfigRepository {
|
|
||||||
override fun getTunnelConfigsFlow(): Flow<TunnelConfigs> {
|
|
||||||
return tunnelConfigDao.getAllFlow()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getAll(): TunnelConfigs {
|
|
||||||
return tunnelConfigDao.getAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun save(tunnelConfig: TunnelConfig) {
|
|
||||||
tunnelConfigDao.save(tunnelConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun delete(tunnelConfig: TunnelConfig) {
|
|
||||||
tunnelConfigDao.delete(tunnelConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun count(): Int {
|
|
||||||
return tunnelConfigDao.count().toInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ package com.zaneschepke.wireguardautotunnel.module
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
import com.zaneschepke.wireguardautotunnel.repository.AppDatabase
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
@@ -16,14 +16,10 @@ import javax.inject.Singleton
|
|||||||
class DatabaseModule {
|
class DatabaseModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideDatabase(
|
fun provideDatabase(@ApplicationContext context : Context) : AppDatabase {
|
||||||
@ApplicationContext context: Context
|
|
||||||
): AppDatabase {
|
|
||||||
return Room.databaseBuilder(
|
return Room.databaseBuilder(
|
||||||
context,
|
context,
|
||||||
AppDatabase::class.java,
|
AppDatabase::class.java, context.getString(R.string.db_name))
|
||||||
context.getString(R.string.db_name)
|
|
||||||
)
|
|
||||||
.fallbackToDestructiveMigration()
|
.fallbackToDestructiveMigration()
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.module
|
|
||||||
|
|
||||||
import javax.inject.Qualifier
|
|
||||||
|
|
||||||
@Qualifier
|
|
||||||
@Retention(AnnotationRetention.BINARY)
|
|
||||||
annotation class Kernel
|
|
||||||
@@ -1,51 +1,27 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.module
|
package com.zaneschepke.wireguardautotunnel.module
|
||||||
|
|
||||||
import android.content.Context
|
import com.zaneschepke.wireguardautotunnel.repository.AppDatabase
|
||||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
||||||
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepositoryImpl
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepositoryImpl
|
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
class RepositoryModule {
|
class RepositoryModule {
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun provideSettingsDoa(appDatabase: AppDatabase): SettingsDao {
|
fun provideSettingsRepository(appDatabase: AppDatabase) : SettingsDoa {
|
||||||
return appDatabase.settingDao()
|
return appDatabase.settingDao()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun provideTunnelConfigDoa(appDatabase: AppDatabase): TunnelConfigDao {
|
fun provideTunnelConfigRepository(appDatabase: AppDatabase) : TunnelConfigDao {
|
||||||
return appDatabase.tunnelConfigDoa()
|
return appDatabase.tunnelConfigDoa()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Singleton
|
|
||||||
@Provides
|
|
||||||
fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao): TunnelConfigRepository {
|
|
||||||
return TunnelConfigRepositoryImpl(tunnelConfigDao)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
@Provides
|
|
||||||
fun provideSettingsRepository(settingsDao: SettingsDao): SettingsRepository {
|
|
||||||
return SettingsRepositoryImpl(settingsDao)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
@Provides
|
|
||||||
fun providePreferencesDataStore(@ApplicationContext context: Context): DataStoreManager {
|
|
||||||
return DataStoreManager(context)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -15,19 +15,20 @@ import dagger.hilt.android.scopes.ServiceScoped
|
|||||||
@Module
|
@Module
|
||||||
@InstallIn(ServiceComponent::class)
|
@InstallIn(ServiceComponent::class)
|
||||||
abstract class ServiceModule {
|
abstract class ServiceModule {
|
||||||
@Binds
|
|
||||||
@ServiceScoped
|
|
||||||
abstract fun provideNotificationService(wireGuardNotification: WireGuardNotification): NotificationService
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@ServiceScoped
|
@ServiceScoped
|
||||||
abstract fun provideWifiService(wifiService: WifiService): NetworkService<WifiService>
|
abstract fun provideNotificationService(wireGuardNotification: WireGuardNotification) : NotificationService
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@ServiceScoped
|
@ServiceScoped
|
||||||
abstract fun provideMobileDataService(mobileDataService: MobileDataService): NetworkService<MobileDataService>
|
abstract fun provideWifiService(wifiService: WifiService) : NetworkService<WifiService>
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@ServiceScoped
|
@ServiceScoped
|
||||||
abstract fun provideEthernetService(ethernetService: EthernetService): NetworkService<EthernetService>
|
abstract fun provideMobileDataService(mobileDataService : MobileDataService) : NetworkService<MobileDataService>
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@ServiceScoped
|
||||||
|
abstract fun provideEthernetService(ethernetService: EthernetService) : NetworkService<EthernetService>
|
||||||
}
|
}
|
||||||
@@ -3,10 +3,6 @@ package com.zaneschepke.wireguardautotunnel.module
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.wireguard.android.backend.Backend
|
import com.wireguard.android.backend.Backend
|
||||||
import com.wireguard.android.backend.GoBackend
|
import com.wireguard.android.backend.GoBackend
|
||||||
import com.wireguard.android.backend.WgQuickBackend
|
|
||||||
import com.wireguard.android.util.RootShell
|
|
||||||
import com.wireguard.android.util.ToolsInstaller
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
@@ -19,40 +15,16 @@ import javax.inject.Singleton
|
|||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
class TunnelModule {
|
class TunnelModule {
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideRootShell(
|
|
||||||
@ApplicationContext context: Context
|
|
||||||
): RootShell {
|
|
||||||
return RootShell(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
@Userspace
|
fun provideBackend(@ApplicationContext context : Context) : Backend {
|
||||||
fun provideUserspaceBackend(
|
|
||||||
@ApplicationContext context: Context
|
|
||||||
): Backend {
|
|
||||||
return GoBackend(context)
|
return GoBackend(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
@Kernel
|
fun provideVpnService(backend: Backend) : VpnService {
|
||||||
fun provideKernelBackend(
|
return WireGuardTunnel(backend)
|
||||||
@ApplicationContext context: Context,
|
|
||||||
rootShell: RootShell
|
|
||||||
): Backend {
|
|
||||||
return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideVpnService(
|
|
||||||
@Userspace userspaceBackend: Backend,
|
|
||||||
@Kernel kernelBackend: Backend,
|
|
||||||
settingsRepository : SettingsRepository
|
|
||||||
): VpnService {
|
|
||||||
return WireGuardTunnel(userspaceBackend, kernelBackend, settingsRepository)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.module
|
|
||||||
|
|
||||||
import javax.inject.Qualifier
|
|
||||||
|
|
||||||
@Qualifier
|
|
||||||
@Retention(AnnotationRetention.BINARY)
|
|
||||||
annotation class Userspace
|
|
||||||
@@ -3,23 +3,32 @@ package com.zaneschepke.wireguardautotunnel.receiver
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.goAsync
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.util.goAsync
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class BootReceiver : BroadcastReceiver() {
|
class BootReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var settingsRepository: SettingsRepository
|
lateinit var settingsRepo : SettingsDoa
|
||||||
override fun onReceive(context: Context?, intent: Intent?) = goAsync {
|
|
||||||
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
|
override fun onReceive(context: Context, intent: Intent) = goAsync {
|
||||||
if(settingsRepository.getSettings().isAutoTunnelEnabled) {
|
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
|
||||||
ServiceManager.startWatcherServiceForeground(context!!)
|
try {
|
||||||
|
val settings = settingsRepo.getAll()
|
||||||
|
if (settings.isNotEmpty()) {
|
||||||
|
val setting = settings.first()
|
||||||
|
if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) {
|
||||||
|
ServiceManager.startWatcherService(context, setting.defaultTunnel!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+14
-14
@@ -3,10 +3,10 @@ package com.zaneschepke.wireguardautotunnel.receiver
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.goAsync
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.goAsync
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@@ -14,19 +14,19 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class NotificationActionReceiver : BroadcastReceiver() {
|
class NotificationActionReceiver : BroadcastReceiver() {
|
||||||
@Inject
|
|
||||||
lateinit var settingsRepository: SettingsRepository
|
|
||||||
|
|
||||||
override fun onReceive(
|
@Inject
|
||||||
context: Context,
|
lateinit var settingsRepo : SettingsDoa
|
||||||
intent: Intent?
|
override fun onReceive(context: Context, intent: Intent?) = goAsync {
|
||||||
) = goAsync {
|
|
||||||
try {
|
try {
|
||||||
val settings = settingsRepository.getSettings()
|
val settings = settingsRepo.getAll()
|
||||||
if (settings.defaultTunnel != null) {
|
if (settings.isNotEmpty()) {
|
||||||
ServiceManager.stopVpnService(context)
|
val setting = settings.first()
|
||||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
if (setting.defaultTunnel != null) {
|
||||||
ServiceManager.startVpnServiceForeground(context, settings.defaultTunnel.toString())
|
ServiceManager.stopVpnService(context)
|
||||||
|
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||||
|
ServiceManager.startVpnService(context, setting.defaultTunnel.toString())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
cancel()
|
cancel()
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.repository
|
||||||
|
|
||||||
|
import androidx.room.AutoMigration
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.TypeConverters
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||||
|
|
||||||
|
@Database(entities = [Settings::class, TunnelConfig::class], version = 3, autoMigrations = [
|
||||||
|
AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3)
|
||||||
|
], exportSchema = true)
|
||||||
|
@TypeConverters(DatabaseListConverters::class)
|
||||||
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
abstract fun settingDao(): SettingsDoa
|
||||||
|
abstract fun tunnelConfigDoa() : TunnelConfigDao
|
||||||
|
}
|
||||||
+3
-4
@@ -1,4 +1,4 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data
|
package com.zaneschepke.wireguardautotunnel.repository
|
||||||
|
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
@@ -9,13 +9,12 @@ class DatabaseListConverters {
|
|||||||
fun listToString(value: MutableList<String>): String {
|
fun listToString(value: MutableList<String>): String {
|
||||||
return Json.encodeToString(value)
|
return Json.encodeToString(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun stringToList(value: String): MutableList<String> {
|
fun stringToList(value: String): MutableList<String> {
|
||||||
if (value.isEmpty()) return mutableListOf()
|
if(value.isEmpty()) return mutableListOf()
|
||||||
return try {
|
return try {
|
||||||
Json.decodeFromString<MutableList<String>>(value)
|
Json.decodeFromString<MutableList<String>>(value)
|
||||||
} catch (e: Exception) {
|
} catch (e : Exception) {
|
||||||
val list = value.split(",").toMutableList()
|
val list = value.split(",").toMutableList()
|
||||||
val json = listToString(list)
|
val json = listToString(list)
|
||||||
Json.decodeFromString<MutableList<String>>(json)
|
Json.decodeFromString<MutableList<String>>(json)
|
||||||
+4
-6
@@ -1,15 +1,16 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data
|
package com.zaneschepke.wireguardautotunnel.repository
|
||||||
|
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Delete
|
import androidx.room.Delete
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface SettingsDao {
|
interface SettingsDoa {
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun save(t: Settings)
|
suspend fun save(t: Settings)
|
||||||
|
|
||||||
@@ -22,9 +23,6 @@ interface SettingsDao {
|
|||||||
@Query("SELECT * FROM settings")
|
@Query("SELECT * FROM settings")
|
||||||
suspend fun getAll(): List<Settings>
|
suspend fun getAll(): List<Settings>
|
||||||
|
|
||||||
@Query("SELECT * FROM settings LIMIT 1")
|
|
||||||
fun getSettingsFlow(): Flow<Settings>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM settings")
|
@Query("SELECT * FROM settings")
|
||||||
fun getAllFlow(): Flow<MutableList<Settings>>
|
fun getAllFlow(): Flow<MutableList<Settings>>
|
||||||
|
|
||||||
+4
-3
@@ -1,15 +1,16 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data
|
package com.zaneschepke.wireguardautotunnel.repository
|
||||||
|
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Delete
|
import androidx.room.Delete
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface TunnelConfigDao {
|
interface TunnelConfigDao{
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun save(t: TunnelConfig)
|
suspend fun save(t: TunnelConfig)
|
||||||
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.repository.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
data class Settings(
|
||||||
|
@PrimaryKey(autoGenerate = true) val id : Int = 0,
|
||||||
|
@ColumnInfo(name = "is_tunnel_enabled") var isAutoTunnelEnabled : Boolean = false,
|
||||||
|
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled") var isTunnelOnMobileDataEnabled : Boolean = false,
|
||||||
|
@ColumnInfo(name = "trusted_network_ssids") var trustedNetworkSSIDs : MutableList<String> = mutableListOf(),
|
||||||
|
@ColumnInfo(name = "default_tunnel") var defaultTunnel : String? = null,
|
||||||
|
@ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled : Boolean = false,
|
||||||
|
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled") var isTunnelOnEthernetEnabled : Boolean = false,
|
||||||
|
@ColumnInfo(name = "is_shortcuts_enabled", defaultValue = "false") var isShortcutsEnabled : Boolean = false,
|
||||||
|
@ColumnInfo(name = "is_battery_saver_enabled", defaultValue = "false") var isBatterySaverEnabled : Boolean = false,
|
||||||
|
@ColumnInfo(name = "is_tunnel_on_wifi_enabled", defaultValue = "false") var isTunnelOnWifiEnabled : Boolean = false,
|
||||||
|
) {
|
||||||
|
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig) : Boolean {
|
||||||
|
return if (defaultTunnel != null) {
|
||||||
|
val defaultConfig = TunnelConfig.from(defaultTunnel!!)
|
||||||
|
(tunnelConfig.id == defaultConfig.id)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-7
@@ -1,4 +1,4 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.model
|
package com.zaneschepke.wireguardautotunnel.repository.model
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
@@ -12,19 +12,20 @@ import java.io.InputStream
|
|||||||
@Entity(indices = [Index(value = ["name"], unique = true)])
|
@Entity(indices = [Index(value = ["name"], unique = true)])
|
||||||
@Serializable
|
@Serializable
|
||||||
data class TunnelConfig(
|
data class TunnelConfig(
|
||||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
@PrimaryKey(autoGenerate = true) val id : Int = 0,
|
||||||
@ColumnInfo(name = "name") var name: String,
|
@ColumnInfo(name = "name") var name : String,
|
||||||
@ColumnInfo(name = "wg_quick") var wgQuick: String
|
@ColumnInfo(name = "wg_quick") var wgQuick : String,
|
||||||
) {
|
){
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return Json.encodeToString(serializer(), this)
|
return Json.encodeToString(serializer(), this)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun from(string: String): TunnelConfig {
|
|
||||||
|
fun from(string : String) : TunnelConfig {
|
||||||
return Json.decodeFromString<TunnelConfig>(string)
|
return Json.decodeFromString<TunnelConfig>(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun configFromQuick(wgQuick: String): Config {
|
fun configFromQuick(wgQuick: String): Config {
|
||||||
val inputStream: InputStream = wgQuick.byteInputStream()
|
val inputStream: InputStream = wgQuick.byteInputStream()
|
||||||
val reader = inputStream.bufferedReader(Charsets.UTF_8)
|
val reader = inputStream.bufferedReader(Charsets.UTF_8)
|
||||||
+6
-7
@@ -6,7 +6,9 @@ import android.os.IBinder
|
|||||||
import androidx.lifecycle.LifecycleService
|
import androidx.lifecycle.LifecycleService
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
||||||
open class ForegroundService : LifecycleService() {
|
open class ForegroundService : LifecycleService() {
|
||||||
|
|
||||||
private var isServiceStarted = false
|
private var isServiceStarted = false
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder? {
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
@@ -15,11 +17,7 @@ open class ForegroundService : LifecycleService() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
intent: Intent?,
|
|
||||||
flags: Int,
|
|
||||||
startId: Int
|
|
||||||
): Int {
|
|
||||||
super.onStartCommand(intent, flags, startId)
|
super.onStartCommand(intent, flags, startId)
|
||||||
Timber.d("onStartCommand executed with startId: $startId")
|
Timber.d("onStartCommand executed with startId: $startId")
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
@@ -43,18 +41,19 @@ open class ForegroundService : LifecycleService() {
|
|||||||
return START_STICKY
|
return START_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
Timber.d("The service has been destroyed")
|
Timber.d("The service has been destroyed")
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun startService(extras: Bundle?) {
|
protected open fun startService(extras : Bundle?) {
|
||||||
if (isServiceStarted) return
|
if (isServiceStarted) return
|
||||||
Timber.d("Starting ${this.javaClass.simpleName}")
|
Timber.d("Starting ${this.javaClass.simpleName}")
|
||||||
isServiceStarted = true
|
isServiceStarted = true
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun stopService(extras: Bundle?) {
|
protected open fun stopService(extras : Bundle?) {
|
||||||
Timber.d("Stopping ${this.javaClass.simpleName}")
|
Timber.d("Stopping ${this.javaClass.simpleName}")
|
||||||
try {
|
try {
|
||||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
|
|||||||
+36
-54
@@ -16,60 +16,44 @@ object ServiceManager {
|
|||||||
.getRunningServices(Integer.MAX_VALUE)
|
.getRunningServices(Integer.MAX_VALUE)
|
||||||
.any { it.service.className == service.name }
|
.any { it.service.className == service.name }
|
||||||
|
|
||||||
fun <T : Service> getServiceState(
|
fun <T : Service> getServiceState(context: Context, cls : Class<T>): ServiceState {
|
||||||
context: Context,
|
|
||||||
cls: Class<T>
|
|
||||||
): ServiceState {
|
|
||||||
val isServiceRunning = context.isServiceRunning(cls)
|
val isServiceRunning = context.isServiceRunning(cls)
|
||||||
return if (isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED
|
return if(isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T : Service> actionOnService(
|
private fun <T : Service> actionOnService(action: Action, context: Context, cls : Class<T>, extras : Map<String,String>? = null) {
|
||||||
action: Action,
|
|
||||||
context: Context,
|
|
||||||
cls: Class<T>,
|
|
||||||
extras: Map<String, String>? = null
|
|
||||||
) {
|
|
||||||
if (getServiceState(context, cls) == ServiceState.STOPPED && action == Action.STOP) return
|
if (getServiceState(context, cls) == ServiceState.STOPPED && action == Action.STOP) return
|
||||||
if (getServiceState(context, cls) == ServiceState.STARTED && action == Action.START) return
|
if (getServiceState(context, cls) == ServiceState.STARTED && action == Action.START) return
|
||||||
val intent =
|
val intent = Intent(context, cls).also {
|
||||||
Intent(context, cls).also {
|
it.action = action.name
|
||||||
it.action = action.name
|
extras?.forEach {(k, v) ->
|
||||||
extras?.forEach { (k, v) ->
|
it.putExtra(k, v)
|
||||||
it.putExtra(k, v)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
intent.component?.javaClass
|
intent.component?.javaClass
|
||||||
try {
|
try {
|
||||||
when (action) {
|
when(action) {
|
||||||
Action.START_FOREGROUND -> {
|
Action.START_FOREGROUND -> {
|
||||||
context.startForegroundService(intent)
|
context.startForegroundService(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
Action.START -> {
|
Action.START -> {
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
Action.STOP -> context.startService(intent)
|
Action.STOP -> context.startService(intent)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e : Exception) {
|
||||||
Timber.e(e.message)
|
Timber.e(e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startVpnService(
|
fun startVpnService(context : Context, tunnelConfig : String) {
|
||||||
context: Context,
|
|
||||||
tunnelConfig: String
|
|
||||||
) {
|
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.START,
|
Action.START,
|
||||||
context,
|
context,
|
||||||
WireGuardTunnelService::class.java,
|
WireGuardTunnelService::class.java,
|
||||||
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig)
|
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
fun stopVpnService(context : Context) {
|
||||||
fun stopVpnService(context: Context) {
|
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.STOP,
|
Action.STOP,
|
||||||
context,
|
context,
|
||||||
@@ -77,43 +61,41 @@ object ServiceManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startVpnServiceForeground(
|
fun startVpnServiceForeground(context : Context, tunnelConfig : String) {
|
||||||
context: Context,
|
|
||||||
tunnelConfig: String
|
|
||||||
) {
|
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.START_FOREGROUND,
|
Action.START_FOREGROUND,
|
||||||
context,
|
context,
|
||||||
WireGuardTunnelService::class.java,
|
WireGuardTunnelService::class.java,
|
||||||
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig)
|
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startWatcherServiceForeground(
|
private fun startWatcherServiceForeground(context : Context, tunnelConfig : String) {
|
||||||
context: Context,
|
|
||||||
) {
|
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.START_FOREGROUND,
|
Action.START, context,
|
||||||
context,
|
WireGuardConnectivityWatcherService::class.java, mapOf(context.
|
||||||
WireGuardConnectivityWatcherService::class.java
|
getString(R.string.tunnel_extras_key) to
|
||||||
)
|
tunnelConfig))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startWatcherService(
|
fun startWatcherService(context : Context, tunnelConfig : String) {
|
||||||
context: Context
|
|
||||||
) {
|
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.START,
|
Action.START, context,
|
||||||
context,
|
WireGuardConnectivityWatcherService::class.java, mapOf(context.
|
||||||
WireGuardConnectivityWatcherService::class.java
|
getString(R.string.tunnel_extras_key) to
|
||||||
)
|
tunnelConfig))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopWatcherService(context: Context) {
|
fun stopWatcherService(context : Context) {
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.STOP,
|
Action.STOP, context,
|
||||||
context,
|
WireGuardConnectivityWatcherService::class.java)
|
||||||
WireGuardConnectivityWatcherService::class.java
|
}
|
||||||
)
|
|
||||||
|
fun toggleWatcherServiceForeground(context: Context, tunnelConfig : String) {
|
||||||
|
when(getServiceState( context,
|
||||||
|
WireGuardConnectivityWatcherService::class.java,)) {
|
||||||
|
ServiceState.STARTED -> stopWatcherService(context)
|
||||||
|
ServiceState.STOPPED -> startWatcherServiceForeground(context, tunnelConfig)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+213
-298
@@ -7,12 +7,12 @@ import android.content.Intent
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import androidx.core.app.ServiceCompat
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
||||||
@@ -20,13 +20,10 @@ import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
|
|||||||
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
|
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@@ -34,334 +31,252 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class WireGuardConnectivityWatcherService : ForegroundService() {
|
class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
private val foregroundId = 122
|
|
||||||
|
|
||||||
@Inject lateinit var wifiService: NetworkService<WifiService>
|
private val foregroundId = 122
|
||||||
|
|
||||||
@Inject lateinit var mobileDataService: NetworkService<MobileDataService>
|
@Inject
|
||||||
|
lateinit var wifiService: NetworkService<WifiService>
|
||||||
|
|
||||||
@Inject lateinit var ethernetService: NetworkService<EthernetService>
|
@Inject
|
||||||
|
lateinit var mobileDataService: NetworkService<MobileDataService>
|
||||||
|
|
||||||
@Inject lateinit var settingsRepository: SettingsRepository
|
@Inject
|
||||||
|
lateinit var ethernetService: NetworkService<EthernetService>
|
||||||
|
|
||||||
@Inject lateinit var notificationService: NotificationService
|
@Inject
|
||||||
|
lateinit var settingsRepo: SettingsDoa
|
||||||
|
|
||||||
@Inject lateinit var vpnService: VpnService
|
@Inject
|
||||||
|
lateinit var notificationService: NotificationService
|
||||||
|
|
||||||
private val networkEventsFlow = MutableStateFlow(WatcherState())
|
@Inject
|
||||||
data class WatcherState(
|
lateinit var vpnService: VpnService
|
||||||
val isWifiConnected: Boolean = false,
|
|
||||||
val isVpnConnected : Boolean = false,
|
|
||||||
val isEthernetConnected: Boolean = false,
|
|
||||||
val isMobileDataConnected: Boolean = false,
|
|
||||||
val currentNetworkSSID: String = "",
|
|
||||||
val settings: Settings = Settings()
|
|
||||||
)
|
|
||||||
|
|
||||||
private lateinit var watcherJob: Job
|
private var isWifiConnected = false
|
||||||
|
private var isEthernetConnected = false
|
||||||
|
private var isMobileDataConnected = false
|
||||||
|
private var currentNetworkSSID = ""
|
||||||
|
|
||||||
private var wakeLock: PowerManager.WakeLock? = null
|
private lateinit var watcherJob: Job
|
||||||
private val tag = this.javaClass.name
|
private lateinit var setting: Settings
|
||||||
|
private lateinit var tunnelConfig: String
|
||||||
|
|
||||||
override fun onCreate() {
|
private var wakeLock: PowerManager.WakeLock? = null
|
||||||
super.onCreate()
|
private val tag = this.javaClass.name
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
|
||||||
try {
|
|
||||||
if(settingsRepository.getSettings().isAutoTunnelPaused) {
|
override fun onCreate() {
|
||||||
launchWatcherPausedNotification()
|
super.onCreate()
|
||||||
} else launchWatcherNotification()
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
} catch (e: Exception) {
|
launchWatcherNotification()
|
||||||
Timber.e("Failed to start watcher service, not enough permissions")
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun startService(extras: Bundle?) {
|
override fun startService(extras: Bundle?) {
|
||||||
super.startService(extras)
|
super.startService(extras)
|
||||||
try {
|
launchWatcherNotification()
|
||||||
// we need this lock so our service gets not affected by Doze Mode
|
val tunnelId = extras?.getString(getString(R.string.tunnel_extras_key))
|
||||||
lifecycleScope.launch { initWakeLock() }
|
if (tunnelId != null) {
|
||||||
cancelWatcherJob()
|
this.tunnelConfig = tunnelId
|
||||||
startWatcherJob()
|
}
|
||||||
} catch (e: Exception) {
|
// we need this lock so our service gets not affected by Doze Mode
|
||||||
Timber.e("Failed to launch watcher service, no permissions")
|
lifecycleScope.launch {
|
||||||
|
initWakeLock()
|
||||||
|
}
|
||||||
|
cancelWatcherJob()
|
||||||
|
if (this::tunnelConfig.isInitialized) {
|
||||||
|
startWatcherJob()
|
||||||
|
} else {
|
||||||
|
stopService(extras)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun stopService(extras: Bundle?) {
|
override fun stopService(extras: Bundle?) {
|
||||||
super.stopService(extras)
|
super.stopService(extras)
|
||||||
wakeLock?.let {
|
wakeLock?.let {
|
||||||
if (it.isHeld) {
|
if (it.isHeld) {
|
||||||
it.release()
|
it.release()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
cancelWatcherJob()
|
||||||
|
stopSelf()
|
||||||
}
|
}
|
||||||
cancelWatcherJob()
|
|
||||||
stopSelf()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchWatcherNotification(description: String = getString(R.string.watcher_notification_text_active)) {
|
private fun launchWatcherNotification() {
|
||||||
val notification =
|
val notification = notificationService.createNotification(
|
||||||
notificationService.createNotification(
|
|
||||||
channelId = getString(R.string.watcher_channel_id),
|
channelId = getString(R.string.watcher_channel_id),
|
||||||
channelName = getString(R.string.watcher_channel_name),
|
channelName = getString(R.string.watcher_channel_name),
|
||||||
title = getString(R.string.auto_tunnel_title),
|
description = getString(R.string.watcher_notification_text),
|
||||||
description = description)
|
vibration = false
|
||||||
ServiceCompat.startForeground(
|
)
|
||||||
this, foregroundId, notification, Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID)
|
super.startForeground(foregroundId, notification)
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchWatcherPausedNotification() {
|
|
||||||
launchWatcherNotification(getString(R.string.watcher_notification_text_paused))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO could this be restarting service in a bad state?
|
//try to start task again if killed
|
||||||
// try to start task again if killed
|
override fun onTaskRemoved(rootIntent: Intent) {
|
||||||
override fun onTaskRemoved(rootIntent: Intent) {
|
Timber.d("Task Removed called")
|
||||||
Timber.d("Task Removed called")
|
val restartServiceIntent = Intent(rootIntent)
|
||||||
val restartServiceIntent = Intent(rootIntent)
|
val restartServicePendingIntent: PendingIntent = PendingIntent.getService(
|
||||||
val restartServicePendingIntent: PendingIntent =
|
this, 1, restartServiceIntent,
|
||||||
PendingIntent.getService(
|
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
|
||||||
this,
|
)
|
||||||
1,
|
applicationContext.getSystemService(Context.ALARM_SERVICE)
|
||||||
restartServiceIntent,
|
val alarmService: AlarmManager =
|
||||||
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE)
|
applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||||
applicationContext.getSystemService(Context.ALARM_SERVICE)
|
alarmService.set(
|
||||||
val alarmService: AlarmManager =
|
AlarmManager.ELAPSED_REALTIME,
|
||||||
applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
SystemClock.elapsedRealtime() + 1000,
|
||||||
alarmService.set(
|
restartServicePendingIntent
|
||||||
AlarmManager.ELAPSED_REALTIME,
|
)
|
||||||
SystemClock.elapsedRealtime() + 1000,
|
|
||||||
restartServicePendingIntent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun initWakeLock() {
|
|
||||||
val isBatterySaverOn =
|
|
||||||
withContext(lifecycleScope.coroutineContext) {
|
|
||||||
settingsRepository.getSettings().isBatterySaverEnabled
|
|
||||||
}
|
|
||||||
wakeLock =
|
|
||||||
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
|
||||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
|
||||||
if (isBatterySaverOn) {
|
|
||||||
Timber.d("Initiating wakelock with timeout")
|
|
||||||
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
|
|
||||||
} else {
|
|
||||||
Timber.d("Initiating wakelock with zero timeout")
|
|
||||||
acquire(Constants.DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cancelWatcherJob() {
|
|
||||||
if (this::watcherJob.isInitialized) {
|
|
||||||
watcherJob.cancel()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun startWatcherJob() {
|
private suspend fun initWakeLock() {
|
||||||
watcherJob =
|
val isBatterySaverOn = withContext(lifecycleScope.coroutineContext) {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
settingsRepo.getAll().firstOrNull()?.isBatterySaverEnabled ?: false
|
||||||
val setting = settingsRepository.getSettings()
|
}
|
||||||
launch {
|
wakeLock =
|
||||||
Timber.d("Starting wifi watcher")
|
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||||
watchForWifiConnectivityChanges()
|
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
||||||
}
|
if (isBatterySaverOn) {
|
||||||
if (setting.isTunnelOnMobileDataEnabled) {
|
Timber.d("Initiating wakelock with timeout")
|
||||||
launch {
|
acquire(Constants.WATCHER_SERVICE_WAKE_LOCK_TIMEOUT)
|
||||||
Timber.d("Starting mobile data watcher")
|
} else {
|
||||||
watchForMobileDataConnectivityChanges()
|
Timber.d("Initiating wakelock with zero timeout")
|
||||||
|
acquire()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (setting.isTunnelOnEthernetEnabled) {
|
|
||||||
launch {
|
private fun cancelWatcherJob() {
|
||||||
Timber.d("Starting ethernet data watcher")
|
if (this::watcherJob.isInitialized) {
|
||||||
watchForEthernetConnectivityChanges()
|
watcherJob.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
launch {
|
|
||||||
Timber.d("Starting vpn state watcher")
|
private fun startWatcherJob() {
|
||||||
watchForVpnConnectivityChanges()
|
watcherJob = lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val settings = settingsRepo.getAll()
|
||||||
|
if (settings.isNotEmpty()) {
|
||||||
|
setting = settings[0]
|
||||||
}
|
}
|
||||||
launch {
|
launch {
|
||||||
Timber.d("Starting settings watcher")
|
watchForWifiConnectivityChanges()
|
||||||
watchForSettingsChanges()
|
|
||||||
}
|
}
|
||||||
launch {
|
if (setting.isTunnelOnMobileDataEnabled) {
|
||||||
Timber.d("Starting management watcher")
|
launch {
|
||||||
manageVpn()
|
watchForMobileDataConnectivityChanges()
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun watchForMobileDataConnectivityChanges() {
|
|
||||||
mobileDataService.networkStatus.collect {
|
|
||||||
when (it) {
|
|
||||||
is NetworkStatus.Available -> {
|
|
||||||
Timber.d("Gained Mobile data connection")
|
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
|
||||||
isMobileDataConnected = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
|
||||||
isMobileDataConnected = true
|
|
||||||
)
|
|
||||||
Timber.d("Mobile data capabilities changed")
|
|
||||||
}
|
|
||||||
is NetworkStatus.Unavailable -> {
|
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
|
||||||
isMobileDataConnected = false
|
|
||||||
)
|
|
||||||
Timber.d("Lost mobile data connection")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private suspend fun watchForSettingsChanges() {
|
|
||||||
settingsRepository.getSettingsFlow().collect {
|
|
||||||
if(networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
|
|
||||||
when(it.isAutoTunnelPaused) {
|
|
||||||
true -> launchWatcherPausedNotification()
|
|
||||||
false -> launchWatcherNotification()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
if (setting.isTunnelOnEthernetEnabled) {
|
||||||
settings = it
|
launch {
|
||||||
)
|
watchForEthernetConnectivityChanges()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
launch {
|
||||||
private suspend fun watchForVpnConnectivityChanges() {
|
manageVpn()
|
||||||
vpnService.vpnState.collect {
|
|
||||||
when(it.status) {
|
|
||||||
Tunnel.State.DOWN -> networkEventsFlow.value = networkEventsFlow.value.copy(
|
|
||||||
isVpnConnected = false
|
|
||||||
)
|
|
||||||
Tunnel.State.UP -> networkEventsFlow.value = networkEventsFlow.value.copy(
|
|
||||||
isVpnConnected = true
|
|
||||||
)
|
|
||||||
else -> {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun watchForEthernetConnectivityChanges() {
|
private suspend fun watchForMobileDataConnectivityChanges() {
|
||||||
ethernetService.networkStatus.collect {
|
mobileDataService.networkStatus.collect {
|
||||||
when (it) {
|
when (it) {
|
||||||
is NetworkStatus.Available -> {
|
is NetworkStatus.Available -> {
|
||||||
Timber.d("Gained Ethernet connection")
|
Timber.d("Gained Mobile data connection")
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
isMobileDataConnected = true
|
||||||
isEthernetConnected = true
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
|
||||||
Timber.d("Ethernet capabilities changed")
|
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
|
||||||
isEthernetConnected = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is NetworkStatus.Unavailable -> {
|
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
|
||||||
isEthernetConnected = false
|
|
||||||
)
|
|
||||||
Timber.d("Lost Ethernet connection")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun watchForWifiConnectivityChanges() {
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
wifiService.networkStatus.collect {
|
isMobileDataConnected = true
|
||||||
when (it) {
|
Timber.d("Mobile data capabilities changed")
|
||||||
is NetworkStatus.Available -> {
|
}
|
||||||
Timber.d("Gained Wi-Fi connection")
|
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
|
||||||
isWifiConnected = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
|
||||||
Timber.d("Wifi capabilities changed")
|
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
|
||||||
isWifiConnected = true
|
|
||||||
)
|
|
||||||
val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: ""
|
|
||||||
Timber.d("Detected SSID: $ssid")
|
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
|
||||||
currentNetworkSSID = ssid
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is NetworkStatus.Unavailable -> {
|
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
|
||||||
isWifiConnected = false
|
|
||||||
)
|
|
||||||
Timber.d("Lost Wi-Fi connection")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO clean this up
|
is NetworkStatus.Unavailable -> {
|
||||||
private suspend fun manageVpn() {
|
isMobileDataConnected = false
|
||||||
networkEventsFlow.collectLatest {
|
Timber.d("Lost mobile data connection")
|
||||||
Timber.i("New watcher state: $it")
|
|
||||||
if (!it.settings.isAutoTunnelPaused && it.settings.defaultTunnel != null) {
|
|
||||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
|
||||||
when {
|
|
||||||
((it.isEthernetConnected &&
|
|
||||||
it.settings.isTunnelOnEthernetEnabled &&
|
|
||||||
!it.isVpnConnected)) -> {
|
|
||||||
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
|
||||||
Timber.i("Condition 1 met")
|
|
||||||
}
|
|
||||||
(!it.isEthernetConnected &&
|
|
||||||
it.settings.isTunnelOnMobileDataEnabled &&
|
|
||||||
!it.isWifiConnected &&
|
|
||||||
it.isMobileDataConnected &&
|
|
||||||
!it.isVpnConnected) -> {
|
|
||||||
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
|
||||||
Timber.i("Condition 2 met")
|
|
||||||
}
|
|
||||||
(!it.isEthernetConnected &&
|
|
||||||
!it.settings.isTunnelOnMobileDataEnabled &&
|
|
||||||
!it.isWifiConnected &&
|
|
||||||
it.isVpnConnected) -> {
|
|
||||||
ServiceManager.stopVpnService(this)
|
|
||||||
Timber.i("Condition 3 met")
|
|
||||||
}
|
|
||||||
(!it.isEthernetConnected &&
|
|
||||||
it.isWifiConnected &&
|
|
||||||
!it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID) &&
|
|
||||||
it.settings.isTunnelOnWifiEnabled &&
|
|
||||||
(!it.isVpnConnected)) -> {
|
|
||||||
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
|
||||||
Timber.i("Condition 4 met")
|
|
||||||
}
|
|
||||||
(!it.isEthernetConnected &&
|
|
||||||
(it.isWifiConnected && it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) &&
|
|
||||||
(it.isVpnConnected)) -> {
|
|
||||||
ServiceManager.stopVpnService(this)
|
|
||||||
Timber.i("Condition 5 met")
|
|
||||||
}
|
|
||||||
(!it.isEthernetConnected &&
|
|
||||||
(it.isWifiConnected &&
|
|
||||||
!it.settings.isTunnelOnWifiEnabled &&
|
|
||||||
(it.isVpnConnected))) -> {
|
|
||||||
ServiceManager.stopVpnService(this)
|
|
||||||
Timber.i("Condition 6 met")
|
|
||||||
}
|
|
||||||
(!it.isEthernetConnected &&
|
|
||||||
!it.isWifiConnected &&
|
|
||||||
!it.isMobileDataConnected &&
|
|
||||||
(it.isVpnConnected)) -> {
|
|
||||||
ServiceManager.stopVpnService(this)
|
|
||||||
Timber.i("Condition 7 met")
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
Timber.i("No condition met")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private suspend fun watchForEthernetConnectivityChanges() {
|
||||||
|
ethernetService.networkStatus.collect {
|
||||||
|
when (it) {
|
||||||
|
is NetworkStatus.Available -> {
|
||||||
|
Timber.d("Gained Ethernet connection")
|
||||||
|
isEthernetConnected = true
|
||||||
|
}
|
||||||
|
|
||||||
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
|
Timber.d("Ethernet capabilities changed")
|
||||||
|
isEthernetConnected = true
|
||||||
|
}
|
||||||
|
|
||||||
|
is NetworkStatus.Unavailable -> {
|
||||||
|
isEthernetConnected = false
|
||||||
|
Timber.d("Lost Ethernet connection")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun watchForWifiConnectivityChanges() {
|
||||||
|
wifiService.networkStatus.collect {
|
||||||
|
when (it) {
|
||||||
|
is NetworkStatus.Available -> {
|
||||||
|
Timber.d("Gained Wi-Fi connection")
|
||||||
|
isWifiConnected = true
|
||||||
|
}
|
||||||
|
|
||||||
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
|
Timber.d("Wifi capabilities changed")
|
||||||
|
isWifiConnected = true
|
||||||
|
currentNetworkSSID = wifiService.getNetworkName(it.networkCapabilities) ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
is NetworkStatus.Unavailable -> {
|
||||||
|
isWifiConnected = false
|
||||||
|
Timber.d("Lost Wi-Fi connection")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun manageVpn() {
|
||||||
|
while (true) {
|
||||||
|
if (isEthernetConnected && setting.isTunnelOnEthernetEnabled && vpnService.getState() == Tunnel.State.DOWN) {
|
||||||
|
ServiceManager.startVpnService(this, tunnelConfig)
|
||||||
|
}
|
||||||
|
if (!isEthernetConnected && setting.isTunnelOnMobileDataEnabled &&
|
||||||
|
!isWifiConnected &&
|
||||||
|
isMobileDataConnected
|
||||||
|
&& vpnService.getState() == Tunnel.State.DOWN
|
||||||
|
) {
|
||||||
|
ServiceManager.startVpnService(this, tunnelConfig)
|
||||||
|
} else if (!isEthernetConnected && !setting.isTunnelOnMobileDataEnabled &&
|
||||||
|
!isWifiConnected &&
|
||||||
|
vpnService.getState() == Tunnel.State.UP
|
||||||
|
) {
|
||||||
|
ServiceManager.stopVpnService(this)
|
||||||
|
} else if (!isEthernetConnected && isWifiConnected &&
|
||||||
|
!setting.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
|
||||||
|
setting.isTunnelOnWifiEnabled &&
|
||||||
|
(vpnService.getState() != Tunnel.State.UP)
|
||||||
|
) {
|
||||||
|
ServiceManager.startVpnService(this, tunnelConfig)
|
||||||
|
} else if (!isEthernetConnected && (isWifiConnected &&
|
||||||
|
setting.trustedNetworkSSIDs.contains(currentNetworkSSID)) &&
|
||||||
|
(vpnService.getState() == Tunnel.State.UP)
|
||||||
|
) {
|
||||||
|
ServiceManager.stopVpnService(this)
|
||||||
|
} else if (!isEthernetConnected && (isWifiConnected &&
|
||||||
|
!setting.isTunnelOnWifiEnabled &&
|
||||||
|
(vpnService.getState() == Tunnel.State.UP)
|
||||||
|
)) {
|
||||||
|
ServiceManager.stopVpnService(this)
|
||||||
|
}
|
||||||
|
delay(Constants.VPN_CONNECTIVITY_CHECK_INTERVAL)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+99
-115
@@ -3,178 +3,162 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
|
|||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.core.app.ServiceCompat
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
|
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class WireGuardTunnelService : ForegroundService() {
|
class WireGuardTunnelService : ForegroundService() {
|
||||||
|
|
||||||
private val foregroundId = 123
|
private val foregroundId = 123
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var vpnService: VpnService
|
lateinit var vpnService : VpnService
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var settingsRepository: SettingsRepository
|
lateinit var settingsRepo: SettingsDoa
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var tunnelConfigRepository: TunnelConfigRepository
|
lateinit var notificationService : NotificationService
|
||||||
|
|
||||||
@Inject
|
private lateinit var job : Job
|
||||||
lateinit var notificationService: NotificationService
|
|
||||||
|
|
||||||
private lateinit var job: Job
|
private var tunnelName : String = ""
|
||||||
|
|
||||||
private var tunnelName: String = ""
|
|
||||||
private var didShowConnected = false
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
if(tunnelConfigRepository.getAll().isNotEmpty()) {
|
launchVpnStartingNotification()
|
||||||
launchVpnNotification()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun startService(extras: Bundle?) {
|
override fun startService(extras : Bundle?) {
|
||||||
super.startService(extras)
|
super.startService(extras)
|
||||||
cancelJob()
|
launchVpnStartingNotification()
|
||||||
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
|
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
|
||||||
val tunnelConfig = tunnelConfigString?.let {
|
cancelJob()
|
||||||
TunnelConfig.from(it)
|
|
||||||
}
|
|
||||||
tunnelName = tunnelConfig?.name ?: ""
|
|
||||||
job = lifecycleScope.launch(Dispatchers.IO) {
|
job = lifecycleScope.launch(Dispatchers.IO) {
|
||||||
launch {
|
launch {
|
||||||
if (tunnelConfig != null) {
|
if(tunnelConfigString != null) {
|
||||||
try {
|
try {
|
||||||
|
val tunnelConfig = TunnelConfig.from(tunnelConfigString)
|
||||||
|
tunnelName = tunnelConfig.name
|
||||||
|
vpnService.startTunnel(tunnelConfig)
|
||||||
|
} catch (e : Exception) {
|
||||||
|
Timber.e("Problem starting tunnel: ${e.message}")
|
||||||
|
stopService(extras)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.d("Tunnel config null, starting default tunnel")
|
||||||
|
val settings = settingsRepo.getAll()
|
||||||
|
if(settings.isNotEmpty()) {
|
||||||
|
val setting = settings[0]
|
||||||
|
if(setting.defaultTunnel != null && setting.isAlwaysOnVpnEnabled) {
|
||||||
|
val tunnelConfig = TunnelConfig.from(setting.defaultTunnel!!)
|
||||||
tunnelName = tunnelConfig.name
|
tunnelName = tunnelConfig.name
|
||||||
vpnService.startTunnel(tunnelConfig)
|
vpnService.startTunnel(tunnelConfig)
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e("Problem starting tunnel: ${e.message}")
|
|
||||||
stopService(extras)
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Timber.d("Tunnel config null, starting default tunnel or first")
|
|
||||||
val settings = settingsRepository.getSettings()
|
|
||||||
val tunnels = tunnelConfigRepository.getAll()
|
|
||||||
if (settings.isAlwaysOnVpnEnabled) {
|
|
||||||
val tunnel = if(settings.defaultTunnel != null) {
|
|
||||||
TunnelConfig.from(settings.defaultTunnel!!)
|
|
||||||
} else if(tunnels.isNotEmpty()) {
|
|
||||||
tunnels.first()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
if(tunnel != null) {
|
|
||||||
tunnelName = tunnel.name
|
|
||||||
vpnService.startTunnel(tunnel)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//TODO add failed to connect notification
|
|
||||||
launch {
|
|
||||||
vpnService.vpnState.collect { state ->
|
|
||||||
state.statistics
|
|
||||||
?.mapPeerStats()
|
|
||||||
?.map { it.value?.handshakeStatus() }
|
|
||||||
.let { statuses ->
|
|
||||||
when {
|
|
||||||
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
|
|
||||||
if(!didShowConnected){
|
|
||||||
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
|
|
||||||
launchVpnNotification(getString(R.string.tunnel_start_title),"${getString(R.string.tunnel_start_text)} $tunnelName")
|
|
||||||
didShowConnected = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
|
|
||||||
statuses?.all { it == HandshakeStatus.NOT_STARTED } == true -> {}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
launch {
|
||||||
|
var didShowConnected = false
|
||||||
|
var didShowFailedHandshakeNotification = false
|
||||||
|
vpnService.handshakeStatus.collect {
|
||||||
|
when(it) {
|
||||||
|
HandshakeStatus.NOT_STARTED -> {
|
||||||
|
}
|
||||||
|
HandshakeStatus.NEVER_CONNECTED -> {
|
||||||
|
if(!didShowFailedHandshakeNotification) {
|
||||||
|
launchVpnConnectionFailedNotification(getString(R.string.initial_connection_failure_message))
|
||||||
|
didShowFailedHandshakeNotification = true
|
||||||
|
didShowConnected = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HandshakeStatus.HEALTHY -> {
|
||||||
|
if(!didShowConnected) {
|
||||||
|
launchVpnConnectedNotification()
|
||||||
|
didShowConnected = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HandshakeStatus.UNHEALTHY -> {
|
||||||
|
if(!didShowFailedHandshakeNotification) {
|
||||||
|
launchVpnConnectionFailedNotification(getString(R.string.lost_connection_failure_message))
|
||||||
|
didShowFailedHandshakeNotification = true
|
||||||
|
didShowConnected = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stopService(extras: Bundle?) {
|
override fun stopService(extras : Bundle?) {
|
||||||
super.stopService(extras)
|
super.stopService(extras)
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
vpnService.stopTunnel()
|
vpnService.stopTunnel()
|
||||||
didShowConnected = false
|
|
||||||
}
|
}
|
||||||
cancelJob()
|
cancelJob()
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchVpnNotification(title : String = getString(R.string.vpn_starting),description : String = getString(R.string.attempt_connection)) {
|
private fun launchVpnConnectedNotification() {
|
||||||
val notification =
|
val notification = notificationService.createNotification(
|
||||||
notificationService.createNotification(
|
channelId = getString(R.string.vpn_channel_id),
|
||||||
channelId = getString(R.string.vpn_channel_id),
|
channelName = getString(R.string.vpn_channel_name),
|
||||||
channelName = getString(R.string.vpn_channel_name),
|
title = getString(R.string.tunnel_start_title),
|
||||||
title = title,
|
onGoing = false,
|
||||||
onGoing = false,
|
vibration = false,
|
||||||
vibration = false,
|
showTimestamp = true,
|
||||||
showTimestamp = true,
|
description = "${getString(R.string.tunnel_start_text)} $tunnelName"
|
||||||
description = description
|
|
||||||
)
|
|
||||||
ServiceCompat.startForeground(
|
|
||||||
this,
|
|
||||||
foregroundId,
|
|
||||||
notification,
|
|
||||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID
|
|
||||||
)
|
)
|
||||||
|
super.startForeground(foregroundId, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchVpnConnectionFailedNotification(message: String) {
|
private fun launchVpnStartingNotification() {
|
||||||
val notification =
|
val notification = notificationService.createNotification(
|
||||||
notificationService.createNotification(
|
channelId = getString(R.string.vpn_channel_id),
|
||||||
channelId = getString(R.string.vpn_channel_id),
|
channelName = getString(R.string.vpn_channel_name),
|
||||||
channelName = getString(R.string.vpn_channel_name),
|
title = getString(R.string.vpn_starting),
|
||||||
action =
|
onGoing = false,
|
||||||
PendingIntent.getBroadcast(
|
vibration = false,
|
||||||
this,
|
showTimestamp = true,
|
||||||
0,
|
description = getString(R.string.attempt_connection)
|
||||||
Intent(this, NotificationActionReceiver::class.java),
|
|
||||||
PendingIntent.FLAG_IMMUTABLE
|
|
||||||
),
|
|
||||||
actionText = getString(R.string.restart),
|
|
||||||
title = getString(R.string.vpn_connection_failed),
|
|
||||||
onGoing = false,
|
|
||||||
vibration = true,
|
|
||||||
showTimestamp = true,
|
|
||||||
description = message
|
|
||||||
)
|
|
||||||
ServiceCompat.startForeground(
|
|
||||||
this,
|
|
||||||
foregroundId,
|
|
||||||
notification,
|
|
||||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID
|
|
||||||
)
|
)
|
||||||
|
super.startForeground(foregroundId, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun launchVpnConnectionFailedNotification(message : String) {
|
||||||
|
val notification = notificationService.createNotification(
|
||||||
|
channelId = getString(R.string.vpn_channel_id),
|
||||||
|
channelName = getString(R.string.vpn_channel_name),
|
||||||
|
action = PendingIntent.getBroadcast(this,0,
|
||||||
|
Intent(this, NotificationActionReceiver::class.java),PendingIntent.FLAG_IMMUTABLE),
|
||||||
|
actionText = getString(R.string.restart),
|
||||||
|
title = getString(R.string.vpn_connection_failed),
|
||||||
|
onGoing = false,
|
||||||
|
vibration = true,
|
||||||
|
showTimestamp = true,
|
||||||
|
description = message
|
||||||
|
)
|
||||||
|
super.startForeground(foregroundId, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun cancelJob() {
|
private fun cancelJob() {
|
||||||
if (this::job.isInitialized) {
|
if(this::job.isInitialized) {
|
||||||
job.cancel()
|
job.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+59
-78
@@ -14,82 +14,69 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
|
||||||
val context: Context,
|
abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Context, networkCapability : Int) : NetworkService<T> {
|
||||||
networkCapability: Int
|
|
||||||
) : NetworkService<T> {
|
|
||||||
private val connectivityManager =
|
private val connectivityManager =
|
||||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
|
||||||
private val wifiManager =
|
private val wifiManager =
|
||||||
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
||||||
|
|
||||||
override val networkStatus =
|
override val networkStatus = callbackFlow {
|
||||||
callbackFlow {
|
val networkStatusCallback = when (Build.VERSION.SDK_INT) {
|
||||||
val networkStatusCallback =
|
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
|
||||||
when (Build.VERSION.SDK_INT) {
|
object : ConnectivityManager.NetworkCallback(
|
||||||
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
|
FLAG_INCLUDE_LOCATION_INFO
|
||||||
object : ConnectivityManager.NetworkCallback(
|
) {
|
||||||
FLAG_INCLUDE_LOCATION_INFO
|
override fun onAvailable(network: Network) {
|
||||||
) {
|
trySend(NetworkStatus.Available(network))
|
||||||
override fun onAvailable(network: Network) {
|
|
||||||
trySend(NetworkStatus.Available(network))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLost(network: Network) {
|
|
||||||
trySend(NetworkStatus.Unavailable(network))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCapabilitiesChanged(
|
|
||||||
network: Network,
|
|
||||||
networkCapabilities: NetworkCapabilities
|
|
||||||
) {
|
|
||||||
trySend(
|
|
||||||
NetworkStatus.CapabilitiesChanged(
|
|
||||||
network,
|
|
||||||
networkCapabilities
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
override fun onLost(network: Network) {
|
||||||
object : ConnectivityManager.NetworkCallback() {
|
trySend(NetworkStatus.Unavailable(network))
|
||||||
override fun onAvailable(network: Network) {
|
}
|
||||||
trySend(NetworkStatus.Available(network))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLost(network: Network) {
|
override fun onCapabilitiesChanged(
|
||||||
trySend(NetworkStatus.Unavailable(network))
|
network: Network,
|
||||||
}
|
networkCapabilities: NetworkCapabilities
|
||||||
|
) {
|
||||||
override fun onCapabilitiesChanged(
|
trySend(NetworkStatus.CapabilitiesChanged(network, networkCapabilities))
|
||||||
network: Network,
|
|
||||||
networkCapabilities: NetworkCapabilities
|
|
||||||
) {
|
|
||||||
trySend(
|
|
||||||
NetworkStatus.CapabilitiesChanged(
|
|
||||||
network,
|
|
||||||
networkCapabilities
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val request =
|
}
|
||||||
NetworkRequest.Builder()
|
|
||||||
.addTransportType(networkCapability)
|
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
|
||||||
.build()
|
|
||||||
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
|
|
||||||
|
|
||||||
awaitClose {
|
else -> {
|
||||||
connectivityManager.unregisterNetworkCallback(networkStatusCallback)
|
object : ConnectivityManager.NetworkCallback() {
|
||||||
|
|
||||||
|
override fun onAvailable(network: Network) {
|
||||||
|
trySend(NetworkStatus.Available(network))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLost(network: Network) {
|
||||||
|
trySend(NetworkStatus.Unavailable(network))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCapabilitiesChanged(
|
||||||
|
network: Network,
|
||||||
|
networkCapabilities: NetworkCapabilities
|
||||||
|
) {
|
||||||
|
trySend(NetworkStatus.CapabilitiesChanged(network, networkCapabilities))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val request = NetworkRequest.Builder()
|
||||||
|
.addTransportType(networkCapability)
|
||||||
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||||
|
.build()
|
||||||
|
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
|
||||||
|
|
||||||
|
awaitClose {
|
||||||
|
connectivityManager.unregisterNetworkCallback(networkStatusCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? {
|
override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? {
|
||||||
var ssid: String? = getWifiNameFromCapabilities(networkCapabilities)
|
var ssid: String? = getWifiNameFromCapabilities(networkCapabilities)
|
||||||
@@ -102,6 +89,7 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
|||||||
return ssid?.trim('"')
|
return ssid?.trim('"')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private fun getWifiNameFromCapabilities(networkCapabilities: NetworkCapabilities): String? {
|
private fun getWifiNameFromCapabilities(networkCapabilities: NetworkCapabilities): String? {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
@@ -117,20 +105,13 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
inline fun <Result> Flow<NetworkStatus>.map(
|
inline fun <Result> Flow<NetworkStatus>.map(
|
||||||
crossinline onUnavailable: suspend (network: Network) -> Result,
|
crossinline onUnavailable: suspend (network : Network) -> Result,
|
||||||
crossinline onAvailable: suspend (network: Network) -> Result,
|
crossinline onAvailable: suspend (network : Network) -> Result,
|
||||||
crossinline onCapabilitiesChanged: suspend (
|
crossinline onCapabilitiesChanged: suspend (network : Network, networkCapabilities : NetworkCapabilities) -> Result,
|
||||||
network: Network,
|
): Flow<Result> = map { status ->
|
||||||
networkCapabilities: NetworkCapabilities
|
when (status) {
|
||||||
) -> Result
|
is NetworkStatus.Unavailable -> onUnavailable(status.network)
|
||||||
): Flow<Result> =
|
is NetworkStatus.Available -> onAvailable(status.network)
|
||||||
map { status ->
|
is NetworkStatus.CapabilitiesChanged -> onCapabilitiesChanged(status.network, status.networkCapabilities)
|
||||||
when (status) {
|
|
||||||
is NetworkStatus.Unavailable -> onUnavailable(status.network)
|
|
||||||
is NetworkStatus.Available -> onAvailable(status.network)
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> onCapabilitiesChanged(
|
|
||||||
status.network,
|
|
||||||
status.networkCapabilities
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
+3
-6
@@ -5,9 +5,6 @@ import android.net.NetworkCapabilities
|
|||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class EthernetService
|
class EthernetService @Inject constructor(@ApplicationContext context: Context) :
|
||||||
@Inject
|
BaseNetworkService<EthernetService>(context, NetworkCapabilities.TRANSPORT_ETHERNET) {
|
||||||
constructor(
|
}
|
||||||
@ApplicationContext context: Context
|
|
||||||
) :
|
|
||||||
BaseNetworkService<EthernetService>(context, NetworkCapabilities.TRANSPORT_ETHERNET)
|
|
||||||
+3
-6
@@ -5,9 +5,6 @@ import android.net.NetworkCapabilities
|
|||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class MobileDataService
|
class MobileDataService @Inject constructor(@ApplicationContext context: Context) :
|
||||||
@Inject
|
BaseNetworkService<MobileDataService>(context, NetworkCapabilities.TRANSPORT_CELLULAR) {
|
||||||
constructor(
|
}
|
||||||
@ApplicationContext context: Context
|
|
||||||
) :
|
|
||||||
BaseNetworkService<MobileDataService>(context, NetworkCapabilities.TRANSPORT_CELLULAR)
|
|
||||||
-1
@@ -5,6 +5,5 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
|
|
||||||
interface NetworkService<T> {
|
interface NetworkService<T> {
|
||||||
fun getNetworkName(networkCapabilities: NetworkCapabilities): String?
|
fun getNetworkName(networkCapabilities: NetworkCapabilities): String?
|
||||||
|
|
||||||
val networkStatus: Flow<NetworkStatus>
|
val networkStatus: Flow<NetworkStatus>
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-6
@@ -4,10 +4,7 @@ import android.net.Network
|
|||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
|
|
||||||
sealed class NetworkStatus {
|
sealed class NetworkStatus {
|
||||||
class Available(val network: Network) : NetworkStatus()
|
class Available(val network : Network) : NetworkStatus()
|
||||||
|
class Unavailable(val network : Network) : NetworkStatus()
|
||||||
class Unavailable(val network: Network) : NetworkStatus()
|
class CapabilitiesChanged(val network : Network, val networkCapabilities : NetworkCapabilities) : NetworkStatus()
|
||||||
|
|
||||||
class CapabilitiesChanged(val network: Network, val networkCapabilities: NetworkCapabilities) :
|
|
||||||
NetworkStatus()
|
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-6
@@ -5,9 +5,6 @@ import android.net.NetworkCapabilities
|
|||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class WifiService
|
class WifiService @Inject constructor(@ApplicationContext context: Context) :
|
||||||
@Inject
|
BaseNetworkService<WifiService>(context, NetworkCapabilities.TRANSPORT_WIFI) {
|
||||||
constructor(
|
}
|
||||||
@ApplicationContext context: Context
|
|
||||||
) :
|
|
||||||
BaseNetworkService<WifiService>(context, NetworkCapabilities.TRANSPORT_WIFI)
|
|
||||||
+2
-4
@@ -3,7 +3,6 @@ package com.zaneschepke.wireguardautotunnel.service.notification
|
|||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
|
|
||||||
interface NotificationService {
|
interface NotificationService {
|
||||||
fun createNotification(
|
fun createNotification(
|
||||||
@@ -13,11 +12,10 @@ interface NotificationService {
|
|||||||
action: PendingIntent? = null,
|
action: PendingIntent? = null,
|
||||||
actionText: String? = null,
|
actionText: String? = null,
|
||||||
description: String,
|
description: String,
|
||||||
showTimestamp: Boolean = false,
|
showTimestamp : Boolean = false,
|
||||||
importance: Int = NotificationManager.IMPORTANCE_HIGH,
|
importance: Int = NotificationManager.IMPORTANCE_HIGH,
|
||||||
vibration: Boolean = false,
|
vibration: Boolean = false,
|
||||||
onGoing: Boolean = true,
|
onGoing: Boolean = true,
|
||||||
lights: Boolean = true,
|
lights: Boolean = true
|
||||||
onlyAlertOnce: Boolean = true,
|
|
||||||
): Notification
|
): Notification
|
||||||
}
|
}
|
||||||
+26
-51
@@ -7,29 +7,14 @@ import android.app.PendingIntent
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.MainActivity
|
import com.zaneschepke.wireguardautotunnel.ui.MainActivity
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class WireGuardNotification
|
class WireGuardNotification @Inject constructor(@ApplicationContext private val context: Context) : NotificationService {
|
||||||
@Inject
|
|
||||||
constructor(
|
|
||||||
@ApplicationContext private val context: Context
|
|
||||||
) : NotificationService {
|
|
||||||
private val notificationManager =
|
|
||||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
|
|
||||||
private val watcherBuilder: NotificationCompat.Builder =
|
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
NotificationCompat.Builder(
|
|
||||||
context,
|
|
||||||
context.getString(R.string.watcher_channel_id)
|
|
||||||
)
|
|
||||||
private val tunnelBuilder: NotificationCompat.Builder = NotificationCompat.Builder(
|
|
||||||
context,
|
|
||||||
context.getString(R.string.vpn_channel_id)
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun createNotification(
|
override fun createNotification(
|
||||||
channelId: String,
|
channelId: String,
|
||||||
@@ -42,22 +27,20 @@ constructor(
|
|||||||
importance: Int,
|
importance: Int,
|
||||||
vibration: Boolean,
|
vibration: Boolean,
|
||||||
onGoing: Boolean,
|
onGoing: Boolean,
|
||||||
lights: Boolean,
|
lights: Boolean
|
||||||
onlyAlertOnce: Boolean,
|
|
||||||
): Notification {
|
): Notification {
|
||||||
val channel =
|
val channel = NotificationChannel(
|
||||||
NotificationChannel(
|
channelId,
|
||||||
channelId,
|
channelName,
|
||||||
channelName,
|
importance
|
||||||
importance
|
).let {
|
||||||
).let {
|
it.description = title
|
||||||
it.description = title
|
it.enableLights(lights)
|
||||||
it.enableLights(lights)
|
it.lightColor = Color.RED
|
||||||
it.lightColor = Color.RED
|
it.enableVibration(vibration)
|
||||||
it.enableVibration(vibration)
|
it.vibrationPattern = longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)
|
||||||
it.vibrationPattern = longArrayOf(100,200,300)
|
it
|
||||||
it
|
}
|
||||||
}
|
|
||||||
notificationManager.createNotificationChannel(channel)
|
notificationManager.createNotificationChannel(channel)
|
||||||
val pendingIntent: PendingIntent =
|
val pendingIntent: PendingIntent =
|
||||||
Intent(context, MainActivity::class.java).let { notificationIntent ->
|
Intent(context, MainActivity::class.java).let { notificationIntent ->
|
||||||
@@ -69,31 +52,23 @@ constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val builder = when(channelId) {
|
val builder: Notification.Builder =
|
||||||
context.getString(R.string.watcher_channel_id) -> watcherBuilder
|
Notification.Builder(
|
||||||
context.getString(R.string.vpn_channel_id) -> tunnelBuilder
|
context,
|
||||||
else -> {
|
channelId
|
||||||
NotificationCompat.Builder(
|
)
|
||||||
context,
|
|
||||||
channelId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.let {
|
return builder.let {
|
||||||
if (action != null && actionText != null) {
|
if(action != null && actionText != null) {
|
||||||
|
//TODO find a not deprecated way to do this
|
||||||
it.addAction(
|
it.addAction(
|
||||||
NotificationCompat.Action.Builder(0, actionText, action)
|
Notification.Action.Builder(0, actionText, action)
|
||||||
.build()
|
.build())
|
||||||
)
|
it.setAutoCancel(true)
|
||||||
it.setAutoCancel(true)
|
|
||||||
}
|
}
|
||||||
it.setContentTitle(title)
|
it.setContentTitle(title)
|
||||||
.setContentText(description)
|
.setContentText(description)
|
||||||
.setOnlyAlertOnce(onlyAlertOnce)
|
|
||||||
.setContentIntent(pendingIntent)
|
.setContentIntent(pendingIntent)
|
||||||
.setOngoing(onGoing)
|
.setOngoing(onGoing)
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
||||||
.setShowWhen(showTimestamp)
|
.setShowWhen(showTimestamp)
|
||||||
.setSmallIcon(R.mipmap.ic_launcher_foreground)
|
.setSmallIcon(R.mipmap.ic_launcher_foreground)
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
+40
-41
@@ -1,75 +1,66 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.shortcut
|
package com.zaneschepke.wireguardautotunnel.service.shortcut
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
|
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ShortcutsActivity : ComponentActivity() {
|
class ShortcutsActivity : ComponentActivity() {
|
||||||
@Inject
|
|
||||||
lateinit var settingsRepository: SettingsRepository
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var tunnelConfigRepository: TunnelConfigRepository
|
lateinit var settingsRepo : SettingsDoa
|
||||||
|
|
||||||
private suspend fun toggleWatcherServicePause() {
|
@Inject
|
||||||
val settings = settingsRepository.getSettings()
|
lateinit var tunnelConfigRepo : TunnelConfigDao
|
||||||
if (settings.isAutoTunnelEnabled) {
|
|
||||||
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
private fun attemptWatcherServiceToggle(tunnelConfig : String) {
|
||||||
settingsRepository.save(settings.copy(
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
isAutoTunnelPaused = pauseAutoTunnel
|
val settings = getSettings()
|
||||||
))
|
if(settings.isAutoTunnelEnabled) {
|
||||||
|
ServiceManager.toggleWatcherServiceForeground(this@ShortcutsActivity, tunnelConfig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(View(this))
|
if(intent.getStringExtra(CLASS_NAME_EXTRA_KEY)
|
||||||
if (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)
|
.equals(WireGuardTunnelService::class.java.simpleName)) {
|
||||||
.equals(WireGuardTunnelService::class.java.simpleName)
|
|
||||||
) {
|
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
val settings = settingsRepository.getSettings()
|
val settings = getSettings()
|
||||||
if (settings.isShortcutsEnabled) {
|
if(settings.isShortcutsEnabled) {
|
||||||
try {
|
try {
|
||||||
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
|
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
|
||||||
val tunnelConfig =
|
val tunnelConfig = if(tunnelName != null) {
|
||||||
if (tunnelName != null) {
|
tunnelConfigRepo.getAll().firstOrNull { it.name == tunnelName }
|
||||||
tunnelConfigRepository.getAll().firstOrNull { it.name == tunnelName }
|
} else {
|
||||||
|
if(settings.defaultTunnel == null) {
|
||||||
|
tunnelConfigRepo.getAll().first()
|
||||||
} else {
|
} else {
|
||||||
if (settings.defaultTunnel == null) {
|
TunnelConfig.from(settings.defaultTunnel!!)
|
||||||
tunnelConfigRepository.getAll().first()
|
|
||||||
} else {
|
|
||||||
TunnelConfig.from(settings.defaultTunnel!!)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
tunnelConfig ?: return@launch
|
|
||||||
toggleWatcherServicePause()
|
|
||||||
when (intent.action) {
|
|
||||||
Action.STOP.name -> ServiceManager.stopVpnService(
|
|
||||||
this@ShortcutsActivity
|
|
||||||
)
|
|
||||||
Action.START.name -> ServiceManager.startVpnServiceForeground(
|
|
||||||
this@ShortcutsActivity,
|
|
||||||
tunnelConfig.toString()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
tunnelConfig ?: return@launch
|
||||||
|
attemptWatcherServiceToggle(tunnelConfig.toString())
|
||||||
|
when(intent.action){
|
||||||
|
Action.STOP.name -> ServiceManager.stopVpnService(this@ShortcutsActivity)
|
||||||
|
Action.START.name -> ServiceManager.startVpnService(this@ShortcutsActivity, tunnelConfig.toString())
|
||||||
|
}
|
||||||
|
} catch (e : Exception) {
|
||||||
Timber.e(e.message)
|
Timber.e(e.message)
|
||||||
finish()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,6 +68,14 @@ class ShortcutsActivity : ComponentActivity() {
|
|||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun getSettings() : Settings {
|
||||||
|
val settings = settingsRepo.getAll()
|
||||||
|
return if (settings.isNotEmpty()) {
|
||||||
|
settings.first()
|
||||||
|
} else {
|
||||||
|
throw WgTunnelException("Settings empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
companion object {
|
companion object {
|
||||||
const val TUNNEL_NAME_EXTRA_KEY = "tunnelName"
|
const val TUNNEL_NAME_EXTRA_KEY = "tunnelName"
|
||||||
const val CLASS_NAME_EXTRA_KEY = "className"
|
const val CLASS_NAME_EXTRA_KEY = "className"
|
||||||
|
|||||||
+84
-67
@@ -4,67 +4,51 @@ import android.os.Build
|
|||||||
import android.service.quicksettings.Tile
|
import android.service.quicksettings.Tile
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class TunnelControlTile() : TileService() {
|
class TunnelControlTile : TileService() {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var tunnelConfigRepository: TunnelConfigRepository
|
lateinit var settingsRepo : SettingsDoa
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var settingsRepository: SettingsRepository
|
lateinit var configRepo : TunnelConfigDao
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var vpnService: VpnService
|
lateinit var vpnService : VpnService
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO)
|
private val scope = CoroutineScope(Dispatchers.Main)
|
||||||
|
|
||||||
private var tunnelName : String? = null
|
private lateinit var job : Job
|
||||||
|
|
||||||
override fun onStartListening() {
|
override fun onStartListening() {
|
||||||
super.onStartListening()
|
job = scope.launch {
|
||||||
Timber.d("On start listening called")
|
updateTileState()
|
||||||
scope.launch {
|
|
||||||
vpnService.vpnState.collect {
|
|
||||||
when(it.status) {
|
|
||||||
Tunnel.State.UP -> setActive()
|
|
||||||
Tunnel.State.DOWN -> setInactive()
|
|
||||||
else -> setInactive()
|
|
||||||
}
|
|
||||||
val tunnels = tunnelConfigRepository.getAll()
|
|
||||||
if(tunnels.isEmpty()) {
|
|
||||||
setUnavailable()
|
|
||||||
return@collect
|
|
||||||
}
|
|
||||||
tunnelName = it.name.ifBlank {
|
|
||||||
val settings = settingsRepository.getSettings()
|
|
||||||
if (settings.defaultTunnel != null) {
|
|
||||||
TunnelConfig.from(settings.defaultTunnel!!).name
|
|
||||||
} else tunnels.firstOrNull()?.name
|
|
||||||
}
|
|
||||||
setTileDescription(tunnelName ?: "")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
super.onStartListening()
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
scope.cancel()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTileRemoved() {
|
override fun onTileRemoved() {
|
||||||
super.onTileRemoved()
|
super.onTileRemoved()
|
||||||
|
cancelJob()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,17 +57,16 @@ class TunnelControlTile() : TileService() {
|
|||||||
unlockAndRun {
|
unlockAndRun {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
val tunnelConfig = tunnelConfigRepository.getAll().first { it.name == tunnelName }
|
val tunnel = determineTileTunnel()
|
||||||
toggleWatcherServicePause()
|
if(tunnel != null) {
|
||||||
if (vpnService.getState() == Tunnel.State.UP) {
|
attemptWatcherServiceToggle(tunnel.toString())
|
||||||
ServiceManager.stopVpnService(this@TunnelControlTile)
|
if(vpnService.getState() == Tunnel.State.UP) {
|
||||||
} else {
|
ServiceManager.stopVpnService(this@TunnelControlTile)
|
||||||
ServiceManager.startVpnServiceForeground(
|
} else {
|
||||||
this@TunnelControlTile,
|
ServiceManager.startVpnServiceForeground(this@TunnelControlTile, tunnel.toString())
|
||||||
tunnelConfig.toString()
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e : Exception) {
|
||||||
Timber.e(e.message)
|
Timber.e(e.message)
|
||||||
} finally {
|
} finally {
|
||||||
cancel()
|
cancel()
|
||||||
@@ -92,40 +75,74 @@ class TunnelControlTile() : TileService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toggleWatcherServicePause() {
|
private suspend fun determineTileTunnel() : TunnelConfig? {
|
||||||
|
var tunnelConfig : TunnelConfig? = null
|
||||||
|
val settings = settingsRepo.getAll()
|
||||||
|
if (settings.isNotEmpty()) {
|
||||||
|
val setting = settings.first()
|
||||||
|
tunnelConfig = if (setting.defaultTunnel != null) {
|
||||||
|
TunnelConfig.from(setting.defaultTunnel!!)
|
||||||
|
} else {
|
||||||
|
val configs = configRepo.getAll()
|
||||||
|
val config = if(configs.isNotEmpty()) {
|
||||||
|
configs.first()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tunnelConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun attemptWatcherServiceToggle(tunnelConfig : String) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val settings = settingsRepository.getSettings()
|
val settings = settingsRepo.getAll()
|
||||||
if (settings.isAutoTunnelEnabled) {
|
if (settings.isNotEmpty()) {
|
||||||
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
val setting = settings.first()
|
||||||
settingsRepository.save(settings.copy(
|
if(setting.isAutoTunnelEnabled) {
|
||||||
isAutoTunnelPaused = pauseAutoTunnel
|
ServiceManager.toggleWatcherServiceForeground(this@TunnelControlTile, tunnelConfig)
|
||||||
))
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setActive() {
|
private suspend fun updateTileState() {
|
||||||
qsTile.state = Tile.STATE_ACTIVE
|
vpnService.state.collect {
|
||||||
qsTile.updateTile()
|
try {
|
||||||
|
when(it) {
|
||||||
|
Tunnel.State.UP -> {
|
||||||
|
qsTile.state = Tile.STATE_ACTIVE
|
||||||
|
}
|
||||||
|
Tunnel.State.DOWN -> {
|
||||||
|
qsTile.state = Tile.STATE_INACTIVE
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val config = determineTileTunnel()
|
||||||
|
setTileDescription(config?.name ?: this.resources.getString(R.string.no_tunnel_available))
|
||||||
|
qsTile.updateTile()
|
||||||
|
} catch (e : Exception) {
|
||||||
|
Timber.e("Unable to update tile state")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setInactive() {
|
private fun setTileDescription(description : String) {
|
||||||
qsTile.state = Tile.STATE_INACTIVE
|
|
||||||
qsTile.updateTile()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setUnavailable() {
|
|
||||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
|
||||||
qsTile.updateTile()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setTileDescription(description: String) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
qsTile.subtitle = description
|
qsTile.subtitle = description
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
qsTile.stateDescription = description
|
qsTile.stateDescription = description
|
||||||
}
|
}
|
||||||
qsTile.updateTile()
|
}
|
||||||
|
|
||||||
|
private fun cancelJob() {
|
||||||
|
if(this::job.isInitialized) {
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+5
-7
@@ -2,15 +2,13 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
|
|||||||
|
|
||||||
enum class HandshakeStatus {
|
enum class HandshakeStatus {
|
||||||
HEALTHY,
|
HEALTHY,
|
||||||
STALE,
|
UNHEALTHY,
|
||||||
UNKNOWN,
|
NEVER_CONNECTED,
|
||||||
NOT_STARTED
|
NOT_STARTED;
|
||||||
;
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC = 180
|
private const val WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC = 120
|
||||||
const val STATUS_CHANGE_TIME_BUFFER = 30
|
const val UNHEALTHY_TIME_LIMIT_SEC = WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC + 60
|
||||||
const val STALE_TIME_LIMIT_SEC = WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC + STATUS_CHANGE_TIME_BUFFER
|
|
||||||
const val NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC = 30
|
const val NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC = 30
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,18 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||||
|
|
||||||
|
import com.wireguard.android.backend.Statistics
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.wireguard.crypto.Key
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
|
||||||
interface VpnService : Tunnel {
|
interface VpnService : Tunnel {
|
||||||
suspend fun startTunnel(tunnelConfig: TunnelConfig): Tunnel.State
|
suspend fun startTunnel(tunnelConfig : TunnelConfig) : Tunnel.State
|
||||||
|
|
||||||
suspend fun stopTunnel()
|
suspend fun stopTunnel()
|
||||||
|
val state : SharedFlow<Tunnel.State>
|
||||||
val vpnState: StateFlow<VpnState>
|
val tunnelName : SharedFlow<String>
|
||||||
|
val statistics : SharedFlow<Statistics>
|
||||||
fun getState(): Tunnel.State
|
val lastHandshake : SharedFlow<Map<Key,Long>>
|
||||||
|
val handshakeStatus : SharedFlow<HandshakeStatus>
|
||||||
|
fun getState() : Tunnel.State
|
||||||
}
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
|
||||||
|
|
||||||
import com.wireguard.android.backend.Statistics
|
|
||||||
import com.wireguard.android.backend.Tunnel
|
|
||||||
|
|
||||||
data class VpnState(
|
|
||||||
val status : Tunnel.State = Tunnel.State.DOWN,
|
|
||||||
val name : String = "",
|
|
||||||
val statistics : Statistics? = null
|
|
||||||
)
|
|
||||||
+87
-94
@@ -3,147 +3,140 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
|
|||||||
import com.wireguard.android.backend.Backend
|
import com.wireguard.android.backend.Backend
|
||||||
import com.wireguard.android.backend.BackendException
|
import com.wireguard.android.backend.BackendException
|
||||||
import com.wireguard.android.backend.Statistics
|
import com.wireguard.android.backend.Statistics
|
||||||
import com.wireguard.android.backend.Tunnel.State
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.crypto.Key
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.module.Kernel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.module.Userspace
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class WireGuardTunnel
|
|
||||||
@Inject
|
class WireGuardTunnel @Inject constructor(private val backend : Backend) : VpnService {
|
||||||
constructor(
|
|
||||||
@Userspace private val userspaceBackend: Backend,
|
private val _tunnelName = MutableStateFlow("")
|
||||||
@Kernel private val kernelBackend: Backend,
|
override val tunnelName get() = _tunnelName.asStateFlow()
|
||||||
private val settingsRepository: SettingsRepository
|
|
||||||
) : VpnService {
|
private val _state = MutableSharedFlow<Tunnel.State>(
|
||||||
private val _vpnState = MutableStateFlow(VpnState())
|
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||||
override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow()
|
replay = 1)
|
||||||
|
|
||||||
|
private val _handshakeStatus = MutableSharedFlow<HandshakeStatus>(replay = 1,
|
||||||
|
onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||||
|
override val state get() = _state.asSharedFlow()
|
||||||
|
|
||||||
|
private val _statistics = MutableSharedFlow<Statistics>(replay = 1)
|
||||||
|
override val statistics get() = _statistics.asSharedFlow()
|
||||||
|
|
||||||
|
private val _lastHandshake = MutableSharedFlow<Map<Key, Long>>(replay = 1)
|
||||||
|
override val lastHandshake get() = _lastHandshake.asSharedFlow()
|
||||||
|
|
||||||
|
override val handshakeStatus: SharedFlow<HandshakeStatus>
|
||||||
|
get() = _handshakeStatus.asSharedFlow()
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO)
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
private lateinit var statsJob: Job
|
private lateinit var statsJob : Job
|
||||||
|
|
||||||
private var config: Config? = null
|
|
||||||
|
|
||||||
private var backend: Backend = userspaceBackend
|
override suspend fun startTunnel(tunnelConfig: TunnelConfig) : Tunnel.State{
|
||||||
|
|
||||||
private var backendIsUserspace = true
|
|
||||||
|
|
||||||
init {
|
|
||||||
scope.launch {
|
|
||||||
settingsRepository.getSettingsFlow().collect {
|
|
||||||
if (it.isKernelEnabled && backendIsUserspace) {
|
|
||||||
Timber.d("Setting kernel backend")
|
|
||||||
backend = kernelBackend
|
|
||||||
backendIsUserspace = false
|
|
||||||
} else if (!it.isKernelEnabled && !backendIsUserspace) {
|
|
||||||
Timber.d("Setting userspace backend")
|
|
||||||
backend = userspaceBackend
|
|
||||||
backendIsUserspace = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig): State {
|
|
||||||
return try {
|
return try {
|
||||||
stopTunnelOnConfigChange(tunnelConfig)
|
stopTunnelOnConfigChange(tunnelConfig)
|
||||||
emitTunnelName(tunnelConfig.name)
|
emitTunnelName(tunnelConfig.name)
|
||||||
config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||||
val state =
|
val state = backend.setState(
|
||||||
backend.setState(
|
this, Tunnel.State.UP, config)
|
||||||
this,
|
_state.emit(state)
|
||||||
State.UP,
|
|
||||||
config
|
|
||||||
)
|
|
||||||
emitTunnelState(state)
|
|
||||||
state
|
state
|
||||||
} catch (e: Exception) {
|
} catch (e : Exception) {
|
||||||
Timber.e("Failed to start tunnel with error: ${e.message}")
|
Timber.e("Failed to start tunnel with error: ${e.message}")
|
||||||
State.DOWN
|
Tunnel.State.DOWN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun emitTunnelState(state: State) {
|
private suspend fun emitTunnelName(name : String) {
|
||||||
_vpnState.tryEmit(
|
_tunnelName.emit(name)
|
||||||
_vpnState.value.copy(
|
|
||||||
status = state
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun emitBackendStatistics(statistics: Statistics) {
|
|
||||||
_vpnState.tryEmit(
|
|
||||||
_vpnState.value.copy(
|
|
||||||
statistics = statistics
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitTunnelName(name: String) {
|
|
||||||
_vpnState.emit(
|
|
||||||
_vpnState.value.copy(
|
|
||||||
name = name
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun stopTunnelOnConfigChange(tunnelConfig: TunnelConfig) {
|
private suspend fun stopTunnelOnConfigChange(tunnelConfig: TunnelConfig) {
|
||||||
if (getState() == State.UP && _vpnState.value.name != tunnelConfig.name) {
|
if(getState() == Tunnel.State.UP && _tunnelName.value != tunnelConfig.name) {
|
||||||
stopTunnel()
|
stopTunnel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getName(): String {
|
override fun getName(): String {
|
||||||
return _vpnState.value.name
|
return _tunnelName.value
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun stopTunnel() {
|
override suspend fun stopTunnel() {
|
||||||
try {
|
try {
|
||||||
if (getState() == State.UP) {
|
if(getState() == Tunnel.State.UP) {
|
||||||
val state = backend.setState(this, State.DOWN, null)
|
val state = backend.setState(this, Tunnel.State.DOWN, null)
|
||||||
emitTunnelState(state)
|
_state.emit(state)
|
||||||
}
|
}
|
||||||
} catch (e: BackendException) {
|
} catch (e : BackendException) {
|
||||||
Timber.e("Failed to stop tunnel with error: ${e.message}")
|
Timber.e("Failed to stop tunnel with error: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getState(): State {
|
override fun getState(): Tunnel.State {
|
||||||
return backend.getState(this)
|
return backend.getState(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStateChange(state: State) {
|
override fun onStateChange(state : Tunnel.State) {
|
||||||
val tunnel = this
|
val tunnel = this
|
||||||
emitTunnelState(state)
|
_state.tryEmit(state)
|
||||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
if(state == Tunnel.State.UP) {
|
||||||
if (state == State.UP) {
|
statsJob = scope.launch {
|
||||||
statsJob =
|
val handshakeMap = HashMap<Key, Long>()
|
||||||
scope.launch {
|
var neverHadHandshakeCounter = 0
|
||||||
while (true) {
|
while (true) {
|
||||||
val statistics = backend.getStatistics(tunnel)
|
val statistics = backend.getStatistics(tunnel)
|
||||||
emitBackendStatistics(statistics)
|
_statistics.emit(statistics)
|
||||||
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
|
statistics.peers().forEach {
|
||||||
|
val handshakeEpoch = statistics.peer(it)?.latestHandshakeEpochMillis ?: 0L
|
||||||
|
handshakeMap[it] = handshakeEpoch
|
||||||
|
if(handshakeEpoch == 0L) {
|
||||||
|
if(neverHadHandshakeCounter >= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) {
|
||||||
|
_handshakeStatus.emit(HandshakeStatus.NEVER_CONNECTED)
|
||||||
|
} else {
|
||||||
|
_handshakeStatus.emit(HandshakeStatus.NOT_STARTED)
|
||||||
|
}
|
||||||
|
if(neverHadHandshakeCounter <= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) {
|
||||||
|
neverHadHandshakeCounter += (1 * Constants.VPN_STATISTIC_CHECK_INTERVAL/1000).toInt()
|
||||||
|
}
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
if((NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch) ?: 0L) >= HandshakeStatus.UNHEALTHY_TIME_LIMIT_SEC) {
|
||||||
|
_handshakeStatus.emit(HandshakeStatus.UNHEALTHY)
|
||||||
|
} else {
|
||||||
|
_handshakeStatus.emit(HandshakeStatus.HEALTHY)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
_lastHandshake.emit(handshakeMap)
|
||||||
|
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (state == State.DOWN) {
|
|
||||||
if (this::statsJob.isInitialized) {
|
|
||||||
statsJob.cancel()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(state == Tunnel.State.DOWN) {
|
||||||
|
if(this::statsJob.isInitialized) {
|
||||||
|
statsJob.cancel()
|
||||||
|
}
|
||||||
|
_handshakeStatus.tryEmit(HandshakeStatus.NOT_STARTED)
|
||||||
|
_lastHandshake.tryEmit(emptyMap())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui
|
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class ActivityViewModel @Inject constructor(
|
|
||||||
private val settingsRepo: SettingsDao,
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -6,11 +6,16 @@ import android.net.Uri
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import android.view.KeyEvent
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.animation.ExitTransition
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.slideInHorizontally
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarData
|
import androidx.compose.material3.SnackbarData
|
||||||
@@ -26,7 +31,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusProperties
|
import androidx.compose.ui.input.key.onKeyEvent
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
@@ -36,6 +41,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
|||||||
import com.google.accompanist.permissions.isGranted
|
import com.google.accompanist.permissions.isGranted
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
import com.wireguard.android.backend.GoBackend
|
import com.wireguard.android.backend.GoBackend
|
||||||
|
import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
|
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
||||||
@@ -46,23 +52,22 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars
|
import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
@OptIn(
|
|
||||||
|
@OptIn(ExperimentalAnimationApi::class,
|
||||||
ExperimentalPermissionsApi::class
|
ExperimentalPermissionsApi::class
|
||||||
)
|
)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContent {
|
setContent {
|
||||||
// val activityViewModel = hiltViewModel<ActivityViewModel>()
|
|
||||||
|
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val focusRequester = remember { FocusRequester()}
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
WireguardAutoTunnelTheme {
|
WireguardAutoTunnelTheme {
|
||||||
TransparentSystemBars()
|
TransparentSystemBars()
|
||||||
@@ -79,62 +84,68 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var vpnIntent by remember { mutableStateOf(GoBackend.VpnService.prepare(this)) }
|
var vpnIntent by remember { mutableStateOf(GoBackend.VpnService.prepare(this)) }
|
||||||
val vpnActivityResultState =
|
val vpnActivityResultState = rememberLauncherForActivityResult(
|
||||||
rememberLauncherForActivityResult(
|
ActivityResultContracts.StartActivityForResult(),
|
||||||
ActivityResultContracts.StartActivityForResult(),
|
onResult = {
|
||||||
onResult = {
|
val accepted = (it.resultCode == RESULT_OK)
|
||||||
val accepted = (it.resultCode == RESULT_OK)
|
if (accepted) {
|
||||||
if (accepted) {
|
vpnIntent = null
|
||||||
vpnIntent = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
LaunchedEffect(vpnIntent) {
|
LaunchedEffect(vpnIntent) {
|
||||||
if (vpnIntent != null) {
|
if (vpnIntent != null) {
|
||||||
vpnActivityResultState.launch(vpnIntent)
|
vpnActivityResultState.launch(vpnIntent)
|
||||||
} else {
|
} else requestNotificationPermission()
|
||||||
requestNotificationPermission()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showSnackBarMessage(message: String) {
|
fun showSnackBarMessage(message : String) {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
val result = snackbarHostState.showSnackbar(
|
val result = snackbarHostState.showSnackbar(
|
||||||
message = message,
|
message = message,
|
||||||
actionLabel = applicationContext.getString(R.string.okay),
|
actionLabel = applicationContext.getString(R.string.okay),
|
||||||
duration = SnackbarDuration.Short
|
duration = SnackbarDuration.Short,
|
||||||
)
|
)
|
||||||
when (result) {
|
when (result) {
|
||||||
SnackbarResult.ActionPerformed, SnackbarResult.Dismissed -> {
|
SnackbarResult.ActionPerformed -> { snackbarHostState.currentSnackbarData?.dismiss() }
|
||||||
snackbarHostState.currentSnackbarData?.dismiss()
|
SnackbarResult.Dismissed -> { snackbarHostState.currentSnackbarData?.dismiss() }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(snackbarHost = {
|
||||||
snackbarHost = {
|
|
||||||
SnackbarHost(snackbarHostState) { snackbarData: SnackbarData ->
|
SnackbarHost(snackbarHostState) { snackbarData: SnackbarData ->
|
||||||
CustomSnackBar(
|
CustomSnackBar(
|
||||||
snackbarData.visuals.message,
|
snackbarData.visuals.message,
|
||||||
isRtl = false,
|
isRtl = false,
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
|
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||||
2.dp
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.focusable().focusProperties { up = focusRequester },
|
modifier = Modifier.onKeyEvent {
|
||||||
bottomBar =
|
if (it.nativeKeyEvent.action == KeyEvent.ACTION_UP) {
|
||||||
if (vpnIntent == null && notificationPermissionState.status.isGranted) {
|
when (it.nativeKeyEvent.keyCode) {
|
||||||
{ BottomNavBar(navController, listOf(
|
KeyEvent.KEYCODE_DPAD_UP -> {
|
||||||
Screen.Main.navItem,
|
try {
|
||||||
Screen.Settings.navItem,
|
focusRequester.requestFocus()
|
||||||
Screen.Support.navItem)) }
|
} catch(e : IllegalStateException) {
|
||||||
|
Timber.e("No D-Pad focus request modifier added to element on screen")
|
||||||
|
}
|
||||||
|
false
|
||||||
|
} else -> {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bottomBar = if (vpnIntent == null && notificationPermissionState.status.isGranted) {
|
||||||
|
{ BottomNavBar(navController, Routes.navItems) }
|
||||||
} else {
|
} else {
|
||||||
{}
|
{}
|
||||||
}
|
},
|
||||||
) { padding ->
|
)
|
||||||
|
{ padding ->
|
||||||
if (vpnIntent != null) {
|
if (vpnIntent != null) {
|
||||||
PermissionRequestFailedScreen(
|
PermissionRequestFailedScreen(
|
||||||
padding = padding,
|
padding = padding,
|
||||||
@@ -151,11 +162,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val intentSettings =
|
val intentSettings =
|
||||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||||
intentSettings.data =
|
intentSettings.data =
|
||||||
Uri.fromParts(
|
Uri.fromParts(Constants.URI_PACKAGE_SCHEME, this.packageName, null)
|
||||||
Constants.URI_PACKAGE_SCHEME,
|
|
||||||
this.packageName,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
startActivity(intentSettings)
|
startActivity(intentSettings)
|
||||||
},
|
},
|
||||||
message = getString(R.string.notification_permission_required),
|
message = getString(R.string.notification_permission_required),
|
||||||
@@ -163,40 +170,65 @@ class MainActivity : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
return@Scaffold
|
return@Scaffold
|
||||||
}
|
}
|
||||||
NavHost(navController, startDestination = Screen.Main.route) {
|
|
||||||
composable(
|
NavHost(navController, startDestination = Routes.Main.name) {
|
||||||
Screen.Main.route,
|
composable(Routes.Main.name, enterTransition = {
|
||||||
) {
|
when (initialState.destination.route) {
|
||||||
MainScreen(padding = padding, focusRequester = focusRequester, showSnackbarMessage = { message ->
|
Routes.Settings.name, Routes.Support.name ->
|
||||||
showSnackBarMessage(message)
|
slideInHorizontally(
|
||||||
}, navController = navController)
|
initialOffsetX = { -Constants.SLIDE_IN_TRANSITION_OFFSET },
|
||||||
}
|
animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION)
|
||||||
composable(Screen.Settings.route,
|
)
|
||||||
) {
|
|
||||||
SettingsScreen(padding = padding, showSnackbarMessage = { message ->
|
else -> {
|
||||||
showSnackBarMessage(message)
|
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
|
||||||
}, focusRequester = focusRequester)
|
}
|
||||||
}
|
|
||||||
composable(Screen.Support.route,
|
|
||||||
) {
|
|
||||||
SupportScreen(padding = padding, focusRequester = focusRequester,
|
|
||||||
showSnackbarMessage = { message ->
|
|
||||||
showSnackBarMessage(message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
composable("${Screen.Config.route}/{id}") {
|
|
||||||
val id = it.arguments?.getString("id")
|
|
||||||
if (!id.isNullOrBlank()) {
|
|
||||||
//https://dagger.dev/hilt/view-model#assisted-injection
|
|
||||||
ConfigScreen(
|
|
||||||
navController = navController,
|
|
||||||
id = id,
|
|
||||||
showSnackbarMessage = { message ->
|
|
||||||
showSnackBarMessage(message)
|
|
||||||
},
|
|
||||||
focusRequester = focusRequester
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
}, exitTransition = {
|
||||||
|
ExitTransition.None
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
MainScreen(padding = padding, showSnackbarMessage = { message -> showSnackBarMessage(message) }, navController = navController)
|
||||||
|
}
|
||||||
|
composable(Routes.Settings.name, enterTransition = {
|
||||||
|
when (initialState.destination.route) {
|
||||||
|
Routes.Main.name ->
|
||||||
|
slideInHorizontally(
|
||||||
|
initialOffsetX = { Constants.SLIDE_IN_TRANSITION_OFFSET },
|
||||||
|
animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION)
|
||||||
|
)
|
||||||
|
|
||||||
|
Routes.Support.name -> {
|
||||||
|
slideInHorizontally(
|
||||||
|
initialOffsetX = { -Constants.SLIDE_IN_TRANSITION_OFFSET },
|
||||||
|
animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) { SettingsScreen(padding = padding, showSnackbarMessage = { message -> showSnackBarMessage(message) }, focusRequester = focusRequester) }
|
||||||
|
composable(Routes.Support.name, enterTransition = {
|
||||||
|
when (initialState.destination.route) {
|
||||||
|
Routes.Settings.name, Routes.Main.name ->
|
||||||
|
slideInHorizontally(
|
||||||
|
initialOffsetX = { Constants.SLIDE_IN_ANIMATION_DURATION },
|
||||||
|
animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION)
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) { SupportScreen(padding = padding, focusRequester) }
|
||||||
|
composable("${Routes.Config.name}/{id}", enterTransition = {
|
||||||
|
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
|
||||||
|
}) { it ->
|
||||||
|
val id = it.arguments?.getString("id")
|
||||||
|
if(!id.isNullOrBlank()) {
|
||||||
|
ConfigScreen(navController = navController, id = id, showSnackbarMessage = { message -> showSnackBarMessage(message) }, focusRequester = focusRequester)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.Home
|
||||||
|
import androidx.compose.material.icons.rounded.QuestionMark
|
||||||
|
import androidx.compose.material.icons.rounded.Settings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
|
||||||
|
|
||||||
|
enum class Routes {
|
||||||
|
Main,
|
||||||
|
Settings,
|
||||||
|
Support,
|
||||||
|
Config;
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val navItems = listOf(
|
||||||
|
BottomNavItem(
|
||||||
|
name = "Tunnels",
|
||||||
|
route = Main.name,
|
||||||
|
icon = Icons.Rounded.Home,
|
||||||
|
),
|
||||||
|
BottomNavItem(
|
||||||
|
name = "Settings",
|
||||||
|
route = Settings.name,
|
||||||
|
icon = Icons.Rounded.Settings,
|
||||||
|
),
|
||||||
|
BottomNavItem(
|
||||||
|
name = "Support",
|
||||||
|
route = Support.name,
|
||||||
|
icon = Icons.Rounded.QuestionMark,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui
|
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.rounded.Home
|
|
||||||
import androidx.compose.material.icons.rounded.QuestionMark
|
|
||||||
import androidx.compose.material.icons.rounded.Settings
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
|
|
||||||
|
|
||||||
sealed class Screen(val route : String) {
|
|
||||||
data object Main: Screen("main") {
|
|
||||||
val navItem = BottomNavItem(
|
|
||||||
name = "Tunnels",
|
|
||||||
route = route,
|
|
||||||
icon = Icons.Rounded.Home
|
|
||||||
)
|
|
||||||
}
|
|
||||||
data object Settings: Screen("settings") {
|
|
||||||
val navItem = BottomNavItem(
|
|
||||||
name = "Settings",
|
|
||||||
route = route,
|
|
||||||
icon = Icons.Rounded.Settings
|
|
||||||
)
|
|
||||||
}
|
|
||||||
data object Support: Screen("support") {
|
|
||||||
val navItem = BottomNavItem(
|
|
||||||
name = "Support",
|
|
||||||
route = route,
|
|
||||||
icon = Icons.Rounded.QuestionMark
|
|
||||||
)
|
|
||||||
}
|
|
||||||
data object Config : Screen("config")
|
|
||||||
|
|
||||||
}
|
|
||||||
+5
-13
@@ -14,25 +14,17 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ClickableIconButton(
|
fun ClickableIconButton(onIconClick : () -> Unit, text : String, icon : ImageVector, enabled : Boolean) {
|
||||||
onClick: () -> Unit,
|
TextButton(onClick = {},
|
||||||
onIconClick: () -> Unit,
|
|
||||||
text: String,
|
|
||||||
icon: ImageVector,
|
|
||||||
enabled: Boolean
|
|
||||||
) {
|
|
||||||
TextButton(
|
|
||||||
onClick = onClick,
|
|
||||||
enabled = enabled
|
enabled = enabled
|
||||||
) {
|
) {
|
||||||
Text(text, Modifier.weight(1f, false))
|
Text(text)
|
||||||
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
|
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = stringResource(R.string.delete),
|
contentDescription = stringResource(R.string.delete),
|
||||||
modifier =
|
modifier = Modifier.size(ButtonDefaults.IconSize).clickable {
|
||||||
Modifier.size(ButtonDefaults.IconSize).weight(1f, false).clickable {
|
if(enabled) {
|
||||||
if (enabled) {
|
|
||||||
onIconClick()
|
onIconClick()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-12
@@ -16,21 +16,13 @@ import androidx.compose.ui.unit.dp
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PermissionRequestFailedScreen(
|
fun PermissionRequestFailedScreen(padding : PaddingValues, onRequestAgain : () -> Unit, message : String, buttonText : String ) {
|
||||||
padding: PaddingValues,
|
|
||||||
onRequestAgain: () -> Unit,
|
|
||||||
message: String,
|
|
||||||
buttonText: String
|
|
||||||
) {
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
Column(
|
Column(horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
modifier =
|
modifier = Modifier
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding)
|
.padding(padding)) {
|
||||||
) {
|
|
||||||
Text(message, textAlign = TextAlign.Center, modifier = Modifier.padding(15.dp))
|
Text(message, textAlign = TextAlign.Center, modifier = Modifier.padding(15.dp))
|
||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
|||||||
@@ -18,23 +18,17 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.wireguard.android.backend.Statistics
|
import com.wireguard.android.backend.Statistics
|
||||||
|
import com.zaneschepke.wireguardautotunnel.toThreeDecimalPlaceString
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.toThreeDecimalPlaceString
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RowListItem(
|
fun RowListItem(icon : @Composable () -> Unit, text : String, onHold : () -> Unit,
|
||||||
icon: @Composable () -> Unit,
|
onClick: () -> Unit, rowButton : @Composable () -> Unit,
|
||||||
text: String,
|
expanded : Boolean, statistics: Statistics?
|
||||||
onHold: () -> Unit,
|
) {
|
||||||
onClick: () -> Unit,
|
|
||||||
rowButton: @Composable () -> Unit,
|
|
||||||
expanded: Boolean,
|
|
||||||
statistics: Statistics?
|
|
||||||
) {
|
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier = Modifier
|
||||||
Modifier
|
|
||||||
.animateContentSize()
|
.animateContentSize()
|
||||||
.clip(RoundedCornerShape(30.dp))
|
.clip(RoundedCornerShape(30.dp))
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
@@ -48,27 +42,22 @@ fun RowListItem(
|
|||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier = Modifier
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 15.dp, vertical = 5.dp),
|
.padding(horizontal = 14.dp, vertical = 5.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(verticalAlignment = Alignment.CenterVertically,) {
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxWidth(.60f)
|
|
||||||
) {
|
|
||||||
icon()
|
icon()
|
||||||
Text(text)
|
Text(text)
|
||||||
}
|
}
|
||||||
rowButton()
|
rowButton()
|
||||||
}
|
}
|
||||||
if (expanded) {
|
if(expanded) {
|
||||||
statistics?.peers()?.forEach {
|
statistics?.peers()?.forEach {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier = Modifier
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(end = 10.dp, bottom = 10.dp, start = 10.dp),
|
.padding(end = 10.dp, bottom = 10.dp, start = 10.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -77,11 +66,9 @@ fun RowListItem(
|
|||||||
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
|
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
|
||||||
val peerTx = statistics.peer(it)!!.txBytes
|
val peerTx = statistics.peer(it)!!.txBytes
|
||||||
val peerRx = statistics.peer(it)!!.rxBytes
|
val peerRx = statistics.peer(it)!!.rxBytes
|
||||||
val peerId = it.toBase64().subSequence(0, 3).toString() + "***"
|
val peerId = it.toBase64().subSequence(0,3).toString() + "***"
|
||||||
val handshakeSec =
|
val handshakeSec = NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch)
|
||||||
NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch)
|
val handshake = if(handshakeSec == null) "never" else "$handshakeSec secs ago"
|
||||||
val handshake =
|
|
||||||
if (handshakeSec == null) "never" else "$handshakeSec secs ago"
|
|
||||||
val peerTxMB = NumberUtils.bytesToMB(peerTx).toThreeDecimalPlaceString()
|
val peerTxMB = NumberUtils.bytesToMB(peerTx).toThreeDecimalPlaceString()
|
||||||
val peerRxMB = NumberUtils.bytesToMB(peerRx).toThreeDecimalPlaceString()
|
val peerRxMB = NumberUtils.bytesToMB(peerRx).toThreeDecimalPlaceString()
|
||||||
val fontSize = 9.sp
|
val fontSize = 9.sp
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ import androidx.compose.ui.text.input.KeyboardType
|
|||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchBar(onQuery: (queryString: String) -> Unit) {
|
fun SearchBar(
|
||||||
|
onQuery : (queryString : String) -> Unit
|
||||||
|
) {
|
||||||
// Immediately update and keep track of query from text field changes.
|
// Immediately update and keep track of query from text field changes.
|
||||||
var query: String by rememberSaveable { mutableStateOf("") }
|
var query: String by rememberSaveable { mutableStateOf("") }
|
||||||
var showClearIcon by rememberSaveable { mutableStateOf(false) }
|
var showClearIcon by rememberSaveable { mutableStateOf(false) }
|
||||||
@@ -62,18 +64,16 @@ fun SearchBar(onQuery: (queryString: String) -> Unit) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
colors =
|
colors = TextFieldDefaults.colors(
|
||||||
TextFieldDefaults.colors(
|
|
||||||
focusedContainerColor = Color.Transparent,
|
focusedContainerColor = Color.Transparent,
|
||||||
unfocusedContainerColor = Color.Transparent,
|
unfocusedContainerColor = Color.Transparent,
|
||||||
disabledContainerColor = Color.Transparent
|
disabledContainerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
placeholder = { Text(text = stringResource(R.string.hint_search_packages)) },
|
placeholder = { Text(text = stringResource(R.string.hint_search_packages)) },
|
||||||
textStyle = MaterialTheme.typography.bodySmall,
|
textStyle = MaterialTheme.typography.bodySmall,
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
||||||
modifier =
|
modifier = Modifier
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape)
|
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape)
|
||||||
)
|
)
|
||||||
|
|||||||
+5
-12
@@ -10,15 +10,9 @@ import androidx.compose.ui.text.input.ImeAction
|
|||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ConfigurationTextBox(
|
fun
|
||||||
value: String,
|
ConfigurationTextBox(value : String, hint : String, onValueChange : (String) -> Unit, keyboardActions : KeyboardActions, label : String, modifier: Modifier) {
|
||||||
hint: String,
|
OutlinedTextField(
|
||||||
onValueChange: (String) -> Unit,
|
|
||||||
keyboardActions: KeyboardActions,
|
|
||||||
label: String,
|
|
||||||
modifier: Modifier
|
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
value = value,
|
value = value,
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
@@ -30,11 +24,10 @@ fun ConfigurationTextBox(
|
|||||||
placeholder = {
|
placeholder = {
|
||||||
Text(hint)
|
Text(hint)
|
||||||
},
|
},
|
||||||
keyboardOptions =
|
keyboardOptions = KeyboardOptions(
|
||||||
KeyboardOptions(
|
|
||||||
capitalization = KeyboardCapitalization.None,
|
capitalization = KeyboardCapitalization.None,
|
||||||
imeAction = ImeAction.Done
|
imeAction = ImeAction.Done
|
||||||
),
|
),
|
||||||
keyboardActions = keyboardActions
|
keyboardActions = keyboardActions,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+3
-10
@@ -12,17 +12,10 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ConfigurationToggle(
|
fun ConfigurationToggle(label : String, enabled : Boolean, checked : Boolean, padding : Dp,
|
||||||
label: String,
|
onCheckChanged : () -> Unit, modifier : Modifier = Modifier) {
|
||||||
enabled: Boolean,
|
|
||||||
checked: Boolean,
|
|
||||||
padding: Dp,
|
|
||||||
onCheckChanged: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier = Modifier
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(padding),
|
.padding(padding),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|||||||
+5
-7
@@ -11,14 +11,12 @@ import androidx.navigation.NavController
|
|||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BottomNavBar(
|
fun BottomNavBar(navController : NavController, bottomNavItems : List<BottomNavItem>) {
|
||||||
navController: NavController,
|
|
||||||
bottomNavItems: List<BottomNavItem>
|
|
||||||
) {
|
|
||||||
val backStackEntry = navController.currentBackStackEntryAsState()
|
val backStackEntry = navController.currentBackStackEntryAsState()
|
||||||
|
|
||||||
NavigationBar(
|
NavigationBar(
|
||||||
containerColor = MaterialTheme.colorScheme.background
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
) {
|
) {
|
||||||
bottomNavItems.forEach { item ->
|
bottomNavItems.forEach { item ->
|
||||||
val selected = item.route == backStackEntry.value?.destination?.route
|
val selected = item.route == backStackEntry.value?.destination?.route
|
||||||
@@ -29,13 +27,13 @@ fun BottomNavBar(
|
|||||||
label = {
|
label = {
|
||||||
Text(
|
Text(
|
||||||
text = item.name,
|
text = item.name,
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = item.icon,
|
imageVector = item.icon,
|
||||||
contentDescription = "${item.name} Icon"
|
contentDescription = "${item.name} Icon",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
+54
-72
@@ -11,87 +11,69 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AuthorizationPrompt(
|
fun AuthorizationPrompt(onSuccess : () -> Unit, onFailure : () -> Unit, onError : (String) -> Unit) {
|
||||||
onSuccess: () -> Unit,
|
|
||||||
onFailure: () -> Unit,
|
|
||||||
onError: (String) -> Unit
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val biometricManager = BiometricManager.from(context)
|
val biometricManager = BiometricManager.from(context)
|
||||||
val bio = biometricManager.canAuthenticate(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
|
val bio = biometricManager.canAuthenticate(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
|
||||||
val isBiometricAvailable =
|
val isBiometricAvailable = remember {
|
||||||
remember {
|
when(bio){
|
||||||
when (bio) {
|
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
|
||||||
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
|
onError("Biometrics not available")
|
||||||
onError("Biometrics not available")
|
false
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
|
|
||||||
onError("Biometrics not created")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
|
|
||||||
onError("Biometric hardware not found")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
|
|
||||||
onError("Biometric security update required")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
|
|
||||||
onError("Biometrics not supported")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
|
|
||||||
onError("Biometrics status unknown")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_SUCCESS -> true
|
|
||||||
else -> false
|
|
||||||
}
|
}
|
||||||
|
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
|
||||||
|
onError("Biometrics not created")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
|
||||||
|
onError("Biometric hardware not found")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
|
||||||
|
onError("Biometric security update required")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
|
||||||
|
onError("Biometrics not supported")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
|
||||||
|
onError("Biometrics status unknown")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
BiometricManager.BIOMETRIC_SUCCESS -> true
|
||||||
|
else -> false
|
||||||
}
|
}
|
||||||
if (isBiometricAvailable) {
|
}
|
||||||
|
if(isBiometricAvailable) {
|
||||||
val executor = remember { ContextCompat.getMainExecutor(context) }
|
val executor = remember { ContextCompat.getMainExecutor(context) }
|
||||||
|
|
||||||
val promptInfo =
|
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||||
BiometricPrompt.PromptInfo.Builder()
|
.setAllowedAuthenticators(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
|
||||||
.setAllowedAuthenticators(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
|
.setTitle("Biometric Authentication")
|
||||||
.setTitle("Biometric Authentication")
|
.setSubtitle("Log in using your biometric credential")
|
||||||
.setSubtitle("Log in using your biometric credential")
|
.build()
|
||||||
.build()
|
|
||||||
|
|
||||||
val biometricPrompt =
|
val biometricPrompt = BiometricPrompt(
|
||||||
BiometricPrompt(
|
context as FragmentActivity,
|
||||||
context as FragmentActivity,
|
executor,
|
||||||
executor,
|
object : BiometricPrompt.AuthenticationCallback() {
|
||||||
object : BiometricPrompt.AuthenticationCallback() {
|
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||||
override fun onAuthenticationError(
|
super.onAuthenticationError(errorCode, errString)
|
||||||
errorCode: Int,
|
onFailure()
|
||||||
errString: CharSequence
|
|
||||||
) {
|
|
||||||
super.onAuthenticationError(errorCode, errString)
|
|
||||||
onFailure()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAuthenticationSucceeded(
|
|
||||||
result: BiometricPrompt.AuthenticationResult
|
|
||||||
) {
|
|
||||||
super.onAuthenticationSucceeded(result)
|
|
||||||
onSuccess()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAuthenticationFailed() {
|
|
||||||
super.onAuthenticationFailed()
|
|
||||||
onFailure()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||||
|
super.onAuthenticationSucceeded(result)
|
||||||
|
onSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthenticationFailed() {
|
||||||
|
super.onAuthenticationFailed()
|
||||||
|
onFailure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
biometricPrompt.authenticate(promptInfo)
|
biometricPrompt.authenticate(promptInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-7
@@ -34,14 +34,11 @@ fun CustomSnackBar(
|
|||||||
containerColor: Color = MaterialTheme.colorScheme.surface
|
containerColor: Color = MaterialTheme.colorScheme.surface
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
Snackbar(
|
Snackbar(containerColor = containerColor,
|
||||||
containerColor = containerColor,
|
modifier = Modifier.fillMaxWidth(
|
||||||
modifier =
|
if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) 1/3f else 2/3f).padding(bottom = 100.dp),
|
||||||
Modifier.fillMaxWidth(
|
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f
|
|
||||||
).padding(bottom = 100.dp),
|
|
||||||
shape = RoundedCornerShape(16.dp)
|
shape = RoundedCornerShape(16.dp)
|
||||||
) {
|
) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalLayoutDirection provides
|
LocalLayoutDirection provides
|
||||||
if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
|
if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
|
||||||
|
|||||||
-22
@@ -1,22 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.common.screen
|
|
||||||
|
|
||||||
import androidx.compose.foundation.focusable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun LoadingScreen() {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Top,
|
|
||||||
modifier = Modifier.fillMaxSize().focusable().padding()) {
|
|
||||||
Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-4
@@ -12,10 +12,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SectionTitle(
|
fun SectionTitle(title : String, padding : Dp) {
|
||||||
title: String,
|
|
||||||
padding: Dp
|
|
||||||
) {
|
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
|
|||||||
@@ -3,27 +3,22 @@ package com.zaneschepke.wireguardautotunnel.ui.models
|
|||||||
import com.wireguard.config.Interface
|
import com.wireguard.config.Interface
|
||||||
|
|
||||||
data class InterfaceProxy(
|
data class InterfaceProxy(
|
||||||
var privateKey: String = "",
|
var privateKey : String = "",
|
||||||
var publicKey: String = "",
|
var publicKey : String = "",
|
||||||
var addresses: String = "",
|
var addresses : String = "",
|
||||||
var dnsServers: String = "",
|
var dnsServers : String = "",
|
||||||
var listenPort: String = "",
|
var listenPort : String = "",
|
||||||
var mtu: String = ""
|
var mtu : String = "",
|
||||||
) {
|
){
|
||||||
companion object {
|
companion object {
|
||||||
fun from(i: Interface): InterfaceProxy {
|
fun from(i : Interface) : InterfaceProxy {
|
||||||
return InterfaceProxy(
|
return InterfaceProxy(
|
||||||
publicKey = i.keyPair.publicKey.toBase64().trim(),
|
publicKey = i.keyPair.publicKey.toBase64().trim(),
|
||||||
privateKey = i.keyPair.privateKey.toBase64().trim(),
|
privateKey = i.keyPair.privateKey.toBase64().trim(),
|
||||||
addresses = i.addresses.joinToString(", ").trim(),
|
addresses = i.addresses.joinToString(", ").trim(),
|
||||||
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
|
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
|
||||||
listenPort = if (i.listenPort.isPresent) {
|
listenPort = if(i.listenPort.isPresent) i.listenPort.get().toString().trim() else "",
|
||||||
i.listenPort.get().toString()
|
mtu = if(i.mtu.isPresent) i.mtu.get().toString().trim() else ""
|
||||||
.trim()
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
},
|
|
||||||
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else ""
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,47 +3,30 @@ package com.zaneschepke.wireguardautotunnel.ui.models
|
|||||||
import com.wireguard.config.Peer
|
import com.wireguard.config.Peer
|
||||||
|
|
||||||
data class PeerProxy(
|
data class PeerProxy(
|
||||||
var publicKey: String = "",
|
var publicKey : String = "",
|
||||||
var preSharedKey: String = "",
|
var preSharedKey : String = "",
|
||||||
var persistentKeepalive: String = "",
|
var persistentKeepalive : String = "",
|
||||||
var endpoint: String = "",
|
var endpoint : String = "",
|
||||||
var allowedIps: String = IPV4_WILDCARD.joinToString(", ").trim()
|
var allowedIps: String = IPV4_WILDCARD.joinToString(", ").trim()
|
||||||
) {
|
){
|
||||||
companion object {
|
companion object {
|
||||||
fun from(peer: Peer): PeerProxy {
|
fun from(peer : Peer) : PeerProxy {
|
||||||
return PeerProxy(
|
return PeerProxy(
|
||||||
publicKey = peer.publicKey.toBase64(),
|
publicKey = peer.publicKey.toBase64(),
|
||||||
preSharedKey = if (peer.preSharedKey.isPresent) {
|
preSharedKey = if(peer.preSharedKey.isPresent) peer.preSharedKey.get().toBase64().trim() else "",
|
||||||
peer.preSharedKey.get().toBase64()
|
persistentKeepalive = if(peer.persistentKeepalive.isPresent) peer.persistentKeepalive.get().toString().trim() else "",
|
||||||
.trim()
|
endpoint = if(peer.endpoint.isPresent) peer.endpoint.get().toString().trim() else "",
|
||||||
} else {
|
|
||||||
""
|
|
||||||
},
|
|
||||||
persistentKeepalive = if (peer.persistentKeepalive.isPresent) {
|
|
||||||
peer.persistentKeepalive.get()
|
|
||||||
.toString().trim()
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
},
|
|
||||||
endpoint = if (peer.endpoint.isPresent) {
|
|
||||||
peer.endpoint.get().toString()
|
|
||||||
.trim()
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
},
|
|
||||||
allowedIps = peer.allowedIps.joinToString(", ").trim()
|
allowedIps = peer.allowedIps.joinToString(", ").trim()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val IPV4_PUBLIC_NETWORKS = setOf(
|
||||||
val IPV4_PUBLIC_NETWORKS =
|
"0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3",
|
||||||
setOf(
|
"64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12",
|
||||||
"0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3",
|
"172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7",
|
||||||
"64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12",
|
"176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16",
|
||||||
"172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7",
|
"192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10",
|
||||||
"176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16",
|
"193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4"
|
||||||
"192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10",
|
)
|
||||||
"193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4"
|
|
||||||
)
|
|
||||||
val IPV4_WILDCARD = setOf("0.0.0.0/0")
|
val IPV4_WILDCARD = setOf("0.0.0.0/0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+504
-392
@@ -49,6 +49,7 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
@@ -64,6 +65,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -71,24 +73,22 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.google.accompanist.drawablepainter.DrawablePainter
|
import com.google.accompanist.drawablepainter.DrawablePainter
|
||||||
|
import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
import com.zaneschepke.wireguardautotunnel.ui.Routes
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
|
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import kotlinx.coroutines.Dispatchers
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
import kotlinx.coroutines.launch
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
import timber.log.Timber
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
|
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
@OptIn(
|
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class,
|
||||||
ExperimentalComposeUiApi::class,
|
ExperimentalFoundationApi::class
|
||||||
ExperimentalMaterial3Api::class,
|
)
|
||||||
ExperimentalFoundationApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ConfigScreen(
|
fun ConfigScreen(
|
||||||
viewModel: ConfigViewModel = hiltViewModel(),
|
viewModel: ConfigViewModel = hiltViewModel(),
|
||||||
@@ -97,454 +97,566 @@ fun ConfigScreen(
|
|||||||
showSnackbarMessage: (String) -> Unit,
|
showSnackbarMessage: (String) -> Unit,
|
||||||
id: String
|
id: String
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
|
||||||
var showApplicationsDialog by remember { mutableStateOf(false) }
|
|
||||||
var showAuthPrompt by remember { mutableStateOf(false) }
|
|
||||||
var isAuthenticated by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
|
|
||||||
|
val tunnel by viewModel.tunnel.collectAsStateWithLifecycle(null)
|
||||||
|
val tunnelName = viewModel.tunnelName.collectAsStateWithLifecycle()
|
||||||
|
val packages by viewModel.packages.collectAsStateWithLifecycle()
|
||||||
|
val checkedPackages by viewModel.checkedPackages.collectAsStateWithLifecycle()
|
||||||
|
val include by viewModel.include.collectAsStateWithLifecycle()
|
||||||
|
val isAllApplicationsEnabled by viewModel.isAllApplicationsEnabled.collectAsStateWithLifecycle()
|
||||||
|
val proxyPeers by viewModel.proxyPeers.collectAsStateWithLifecycle()
|
||||||
|
val proxyInterface by viewModel.interfaceProxy.collectAsStateWithLifecycle()
|
||||||
|
var showApplicationsDialog by remember { mutableStateOf(false) }
|
||||||
|
var showAuthPrompt by remember { mutableStateOf(false) }
|
||||||
|
var isAuthenticated by remember { mutableStateOf(false) }
|
||||||
|
val baseTextBoxModifier = Modifier.onFocusChanged {
|
||||||
|
if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
|
keyboardController?.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val keyboardActions = KeyboardActions(
|
||||||
|
onDone = {
|
||||||
|
keyboardController?.hide()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val keyboardOptions = KeyboardOptions(
|
||||||
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
imeAction = ImeAction.Done
|
||||||
|
)
|
||||||
|
|
||||||
|
val fillMaxHeight = .85f
|
||||||
|
val fillMaxWidth = .85f
|
||||||
|
val screenPadding = 5.dp
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.init(id)
|
scope.launch(Dispatchers.IO) {
|
||||||
}
|
try {
|
||||||
|
viewModel.onScreenLoad(id)
|
||||||
LaunchedEffect(uiState.loading) {
|
} catch (e : Exception) {
|
||||||
if(!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
showSnackbarMessage(e.message!!)
|
||||||
delay(Constants.FOCUS_REQUEST_DELAY)
|
navController.navigate(Routes.Main.name)
|
||||||
focusRequester.requestFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uiState.loading) {
|
|
||||||
LoadingScreen()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() })
|
|
||||||
|
|
||||||
val keyboardOptions =
|
|
||||||
KeyboardOptions(imeAction = ImeAction.Done)
|
|
||||||
|
|
||||||
val fillMaxHeight = .85f
|
|
||||||
val fillMaxWidth = .85f
|
|
||||||
val screenPadding = 5.dp
|
|
||||||
|
|
||||||
val applicationButtonText = {
|
|
||||||
"Tunneling apps: " +
|
|
||||||
if (uiState.isAllApplicationsEnabled) {
|
|
||||||
"all"
|
|
||||||
} else {
|
|
||||||
"${uiState.checkedPackageNames.size} " + (if (uiState.include) "included" else "excluded")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showAuthPrompt) {
|
|
||||||
AuthorizationPrompt(
|
|
||||||
onSuccess = {
|
|
||||||
showAuthPrompt = false
|
|
||||||
isAuthenticated = true
|
|
||||||
},
|
|
||||||
onError = { error ->
|
|
||||||
showAuthPrompt = false
|
|
||||||
showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
|
||||||
},
|
|
||||||
onFailure = {
|
|
||||||
showAuthPrompt = false
|
|
||||||
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showApplicationsDialog) {
|
|
||||||
val sortedPackages =
|
|
||||||
remember(uiState.packages) { uiState.packages.sortedBy { viewModel.getPackageLabel(it) } }
|
|
||||||
AlertDialog(onDismissRequest = { showApplicationsDialog = false }) {
|
|
||||||
Surface(
|
|
||||||
tonalElevation = 2.dp,
|
|
||||||
shadowElevation = 2.dp,
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
color = MaterialTheme.colorScheme.surface,
|
|
||||||
modifier =
|
|
||||||
Modifier.fillMaxWidth()
|
|
||||||
.fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f)) {
|
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
|
||||||
Text(stringResource(id = R.string.tunnel_all))
|
|
||||||
Switch(
|
|
||||||
checked = uiState.isAllApplicationsEnabled,
|
|
||||||
onCheckedChange = { viewModel.onAllApplicationsChange(it) })
|
|
||||||
}
|
|
||||||
if (!uiState.isAllApplicationsEnabled) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
|
||||||
Text(stringResource(id = R.string.include))
|
|
||||||
Checkbox(
|
|
||||||
checked = uiState.include,
|
|
||||||
onCheckedChange = { viewModel.onIncludeChange(!uiState.include) })
|
|
||||||
}
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
|
||||||
Text(stringResource(id = R.string.exclude))
|
|
||||||
Checkbox(
|
|
||||||
checked = !uiState.include,
|
|
||||||
onCheckedChange = { viewModel.onIncludeChange(!uiState.include) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
|
||||||
SearchBar(viewModel::emitQueriedPackages)
|
|
||||||
}
|
|
||||||
Spacer(Modifier.padding(5.dp))
|
|
||||||
LazyColumn(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement = Arrangement.Top,
|
|
||||||
modifier = Modifier.fillMaxHeight(4 / 5f)) {
|
|
||||||
items(sortedPackages, key = { it.packageName }) { pack ->
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
modifier = Modifier.fillMaxSize().padding(5.dp)) {
|
|
||||||
Row(modifier = Modifier.fillMaxWidth(fillMaxWidth)) {
|
|
||||||
val drawable =
|
|
||||||
pack.applicationInfo?.loadIcon(context.packageManager)
|
|
||||||
if (drawable != null) {
|
|
||||||
Image(
|
|
||||||
painter = DrawablePainter(drawable),
|
|
||||||
stringResource(id = R.string.icon),
|
|
||||||
modifier = Modifier.size(50.dp, 50.dp))
|
|
||||||
} else {
|
|
||||||
Icon(
|
|
||||||
Icons.Rounded.Android,
|
|
||||||
stringResource(id = R.string.edit),
|
|
||||||
modifier = Modifier.size(50.dp, 50.dp))
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
viewModel.getPackageLabel(pack),
|
|
||||||
modifier = Modifier.padding(5.dp))
|
|
||||||
}
|
|
||||||
Checkbox(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
checked =
|
|
||||||
(uiState.checkedPackageNames.contains(pack.packageName)),
|
|
||||||
onCheckedChange = {
|
|
||||||
if (it) {
|
|
||||||
viewModel.onAddCheckedPackage(pack.packageName)
|
|
||||||
} else {
|
|
||||||
viewModel.onRemoveCheckedPackage(pack.packageName)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
|
||||||
horizontalArrangement = Arrangement.Center) {
|
|
||||||
TextButton(onClick = { showApplicationsDialog = false }) {
|
|
||||||
Text(stringResource(R.string.done))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
val applicationButtonText = {
|
||||||
floatingActionButtonPosition = FabPosition.End,
|
"Tunneling apps: " +
|
||||||
floatingActionButton = {
|
if (isAllApplicationsEnabled) "all"
|
||||||
val secondaryColor = MaterialTheme.colorScheme.secondary
|
else "${checkedPackages.size} " + (if (include) "included" else "excluded")
|
||||||
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
|
||||||
var fobColor by remember { mutableStateOf(secondaryColor) }
|
}
|
||||||
FloatingActionButton(
|
|
||||||
modifier =
|
if(showAuthPrompt) {
|
||||||
Modifier.padding(bottom = 90.dp).onFocusChanged {
|
AuthorizationPrompt(onSuccess = {
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
showAuthPrompt = false
|
||||||
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
isAuthenticated = true },
|
||||||
}
|
onError = { error ->
|
||||||
},
|
showSnackbarMessage(error)
|
||||||
onClick = {
|
showAuthPrompt = false
|
||||||
viewModel.onSaveAllChanges().let {
|
},
|
||||||
when (it) {
|
onFailure = {
|
||||||
is Result.Success -> {
|
showAuthPrompt = false
|
||||||
showSnackbarMessage(it.data.message)
|
showSnackbarMessage(context.getString(R.string.authentication_failed))
|
||||||
navController.navigate(Screen.Main.route)
|
})
|
||||||
}
|
}
|
||||||
is Result.Error -> showSnackbarMessage(it.error.message)
|
|
||||||
}
|
if (showApplicationsDialog) {
|
||||||
}
|
val sortedPackages = remember(packages) {
|
||||||
},
|
packages.sortedBy { viewModel.getPackageLabel(it) }
|
||||||
containerColor = fobColor,
|
}
|
||||||
shape = RoundedCornerShape(16.dp)) {
|
AlertDialog(onDismissRequest = {
|
||||||
Icon(
|
showApplicationsDialog = false
|
||||||
imageVector = Icons.Rounded.Save,
|
|
||||||
contentDescription = stringResource(id = R.string.save_changes),
|
|
||||||
tint = Color.DarkGray)
|
|
||||||
}
|
|
||||||
}) {
|
}) {
|
||||||
Column {
|
Surface(
|
||||||
Column(
|
tonalElevation = 2.dp,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
shadowElevation = 2.dp,
|
||||||
verticalArrangement = Arrangement.Top,
|
shape = RoundedCornerShape(12.dp),
|
||||||
modifier =
|
color = MaterialTheme.colorScheme.surface,
|
||||||
Modifier.verticalScroll(rememberScrollState()).weight(1f, true).fillMaxSize()) {
|
modifier = Modifier
|
||||||
Surface(
|
.fillMaxWidth()
|
||||||
tonalElevation = 2.dp,
|
.fillMaxHeight(if (isAllApplicationsEnabled) 1 / 5f else 4 / 5f)
|
||||||
shadowElevation = 2.dp,
|
) {
|
||||||
shape = RoundedCornerShape(12.dp),
|
Column(
|
||||||
color = MaterialTheme.colorScheme.surface,
|
modifier = Modifier.fillMaxWidth()
|
||||||
modifier =
|
) {
|
||||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
Row(
|
||||||
Modifier.fillMaxHeight(fillMaxHeight).fillMaxWidth(fillMaxWidth)
|
modifier = Modifier
|
||||||
} else {
|
.fillMaxWidth()
|
||||||
Modifier.fillMaxWidth(fillMaxWidth)
|
.padding(horizontal = 20.dp, vertical = 7.dp),
|
||||||
})
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
.padding(top = 50.dp, bottom = 10.dp)) {
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
Column(
|
) {
|
||||||
|
Text(stringResource(id = R.string.tunnel_all))
|
||||||
|
Switch(
|
||||||
|
checked = isAllApplicationsEnabled,
|
||||||
|
onCheckedChange = {
|
||||||
|
viewModel.onAllApplicationsChange(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!isAllApplicationsEnabled) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(
|
||||||
|
horizontal = 20.dp,
|
||||||
|
vertical = 7.dp
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(stringResource(id = R.string.include))
|
||||||
|
Checkbox(
|
||||||
|
checked = include,
|
||||||
|
onCheckedChange = {
|
||||||
|
viewModel.onIncludeChange(!include)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(stringResource(id = R.string.exclude))
|
||||||
|
Checkbox(
|
||||||
|
checked = !include,
|
||||||
|
onCheckedChange = {
|
||||||
|
viewModel.onIncludeChange(!include)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(
|
||||||
|
horizontal = 20.dp,
|
||||||
|
vertical = 7.dp
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
SearchBar(viewModel::emitQueriedPackages)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.padding(5.dp))
|
||||||
|
LazyColumn(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier.padding(15.dp).focusGroup()) {
|
modifier = Modifier
|
||||||
SectionTitle(
|
.fillMaxHeight(4 / 5f)
|
||||||
stringResource(R.string.interface_), padding = screenPadding)
|
) {
|
||||||
ConfigurationTextBox(
|
items(
|
||||||
value = uiState.tunnelName,
|
sortedPackages,
|
||||||
onValueChange = { value -> viewModel.onTunnelNameChange(value) },
|
key = { it.packageName }) { pack ->
|
||||||
keyboardActions = keyboardActions,
|
Row(
|
||||||
label = stringResource(R.string.name),
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
hint = stringResource(R.string.tunnel_name).lowercase(),
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
modifier =
|
modifier = Modifier
|
||||||
Modifier
|
.fillMaxSize()
|
||||||
.fillMaxWidth()
|
.padding(5.dp)
|
||||||
.focusRequester(focusRequester))
|
) {
|
||||||
OutlinedTextField(
|
Row(
|
||||||
modifier =
|
modifier = Modifier.fillMaxWidth(
|
||||||
Modifier.fillMaxWidth().clickable {
|
fillMaxWidth
|
||||||
showAuthPrompt = true
|
)
|
||||||
},
|
) {
|
||||||
value = uiState.interfaceProxy.privateKey,
|
val drawable =
|
||||||
visualTransformation =
|
pack.applicationInfo?.loadIcon(
|
||||||
if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) ||
|
context.packageManager
|
||||||
isAuthenticated)
|
)
|
||||||
VisualTransformation.None
|
if (drawable != null) {
|
||||||
else PasswordVisualTransformation(),
|
Image(
|
||||||
enabled =
|
painter = DrawablePainter(
|
||||||
(id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
|
drawable
|
||||||
onValueChange = { value -> viewModel.onPrivateKeyChange(value) },
|
),
|
||||||
trailingIcon = {
|
stringResource(id = R.string.icon),
|
||||||
IconButton(
|
modifier = Modifier.size(
|
||||||
modifier = Modifier.focusRequester(FocusRequester.Default),
|
50.dp,
|
||||||
onClick = { viewModel.generateKeyPair() }) {
|
50.dp
|
||||||
Icon(
|
)
|
||||||
Icons.Rounded.Refresh,
|
)
|
||||||
stringResource(R.string.rotate_keys),
|
} else {
|
||||||
tint = Color.White)
|
Icon(
|
||||||
|
Icons.Rounded.Android,
|
||||||
|
stringResource(id = R.string.edit),
|
||||||
|
modifier = Modifier.size(
|
||||||
|
50.dp,
|
||||||
|
50.dp
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
Text(
|
||||||
label = { Text(stringResource(R.string.private_key)) },
|
viewModel.getPackageLabel(pack),
|
||||||
singleLine = true,
|
modifier = Modifier.padding(5.dp)
|
||||||
placeholder = { Text(stringResource(R.string.base64_key)) },
|
)
|
||||||
keyboardOptions = keyboardOptions,
|
|
||||||
keyboardActions = keyboardActions)
|
|
||||||
OutlinedTextField(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.focusRequester(FocusRequester.Default),
|
|
||||||
value = uiState.interfaceProxy.publicKey,
|
|
||||||
enabled = false,
|
|
||||||
onValueChange = {},
|
|
||||||
trailingIcon = {
|
|
||||||
IconButton(
|
|
||||||
modifier = Modifier.focusRequester(FocusRequester.Default),
|
|
||||||
onClick = {
|
|
||||||
clipboardManager.setText(
|
|
||||||
AnnotatedString(uiState.interfaceProxy.publicKey))
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
Icons.Rounded.ContentCopy,
|
|
||||||
stringResource(R.string.copy_public_key),
|
|
||||||
tint = Color.White)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
label = { Text(stringResource(R.string.public_key)) },
|
|
||||||
singleLine = true,
|
|
||||||
placeholder = { Text(stringResource(R.string.base64_key)) },
|
|
||||||
keyboardOptions = keyboardOptions,
|
|
||||||
keyboardActions = keyboardActions)
|
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = uiState.interfaceProxy.addresses,
|
|
||||||
onValueChange = { value ->
|
|
||||||
viewModel.onAddressesChanged(value)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.addresses),
|
|
||||||
hint = stringResource(R.string.comma_separated_list),
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth(3 / 5f)
|
|
||||||
.padding(end = 5.dp))
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = uiState.interfaceProxy.listenPort,
|
|
||||||
onValueChange = { value ->
|
|
||||||
viewModel.onListenPortChanged(value)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.listen_port),
|
|
||||||
hint = stringResource(R.string.random),
|
|
||||||
modifier = Modifier.width(IntrinsicSize.Min))
|
|
||||||
}
|
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = uiState.interfaceProxy.dnsServers,
|
|
||||||
onValueChange = { value ->
|
|
||||||
viewModel.onDnsServersChanged(value)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.dns_servers),
|
|
||||||
hint = stringResource(R.string.comma_separated_list),
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth(3 / 5f)
|
|
||||||
.padding(end = 5.dp))
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = uiState.interfaceProxy.mtu,
|
|
||||||
onValueChange = { value -> viewModel.onMtuChanged(value) },
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.mtu),
|
|
||||||
hint = stringResource(R.string.auto),
|
|
||||||
modifier = Modifier.width(IntrinsicSize.Min))
|
|
||||||
}
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
|
||||||
horizontalArrangement = Arrangement.Center) {
|
|
||||||
TextButton(onClick = { showApplicationsDialog = true }) {
|
|
||||||
Text(applicationButtonText())
|
|
||||||
}
|
}
|
||||||
}
|
Checkbox(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
checked = (checkedPackages.contains(pack.packageName)),
|
||||||
|
onCheckedChange = {
|
||||||
|
if (it) viewModel.onAddCheckedPackage(
|
||||||
|
pack.packageName
|
||||||
|
) else viewModel.onRemoveCheckedPackage(
|
||||||
|
pack.packageName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
uiState.proxyPeers.forEachIndexed { index, peer ->
|
}
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = 5.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
showApplicationsDialog = false
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.done))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (tunnel != null) {
|
||||||
|
Scaffold(
|
||||||
|
floatingActionButtonPosition = FabPosition.End,
|
||||||
|
floatingActionButton = {
|
||||||
|
val secondaryColor = MaterialTheme.colorScheme.secondary
|
||||||
|
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||||
|
var fobColor by remember { mutableStateOf(secondaryColor) }
|
||||||
|
FloatingActionButton(
|
||||||
|
modifier = Modifier.padding(bottom = 90.dp).onFocusChanged {
|
||||||
|
if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
|
fobColor = if (it.isFocused) hoverColor else secondaryColor }
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
viewModel.onSaveAllChanges()
|
||||||
|
navController.navigate(Routes.Main.name)
|
||||||
|
showSnackbarMessage(context.resources.getString(R.string.config_changes_saved))
|
||||||
|
} catch (e : Exception) {
|
||||||
|
Timber.e(e.message)
|
||||||
|
showSnackbarMessage(e.message!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
containerColor = fobColor,
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Save,
|
||||||
|
contentDescription = stringResource(id = R.string.save_changes),
|
||||||
|
tint = Color.DarkGray,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Column {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier = Modifier
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.weight(1f, true)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
shadowElevation = 2.dp,
|
shadowElevation = 2.dp,
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
modifier =
|
modifier = (if (WireGuardAutoTunnel.isRunningOnAndroidTv(context))
|
||||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
Modifier
|
||||||
Modifier.fillMaxHeight(fillMaxHeight).fillMaxWidth(fillMaxWidth)
|
.fillMaxHeight(fillMaxHeight)
|
||||||
} else {
|
.fillMaxWidth(fillMaxWidth)
|
||||||
Modifier.fillMaxWidth(fillMaxWidth)
|
else Modifier.fillMaxWidth(fillMaxWidth)).padding(
|
||||||
})
|
top = 50.dp,
|
||||||
.padding(top = 10.dp, bottom = 10.dp)) {
|
bottom = 10.dp
|
||||||
Column(
|
)
|
||||||
horizontalAlignment = Alignment.Start,
|
) {
|
||||||
verticalArrangement = Arrangement.Top,
|
Column(
|
||||||
modifier =
|
horizontalAlignment = Alignment.Start,
|
||||||
Modifier.padding(horizontal = 15.dp).padding(bottom = 10.dp)) {
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier = Modifier.padding(15.dp).focusGroup()
|
||||||
|
) {
|
||||||
|
SectionTitle(stringResource(R.string.interface_), padding = screenPadding)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = tunnelName.value,
|
||||||
|
onValueChange = { value ->
|
||||||
|
viewModel.onTunnelNameChange(value)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.name),
|
||||||
|
hint = stringResource(R.string.tunnel_name).lowercase(),
|
||||||
|
modifier = baseTextBoxModifier.fillMaxWidth().focusRequester(focusRequester)
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = baseTextBoxModifier.fillMaxWidth().clickable {
|
||||||
|
showAuthPrompt = true
|
||||||
|
},
|
||||||
|
value = proxyInterface.privateKey,
|
||||||
|
visualTransformation = if((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
|
enabled = (id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
|
||||||
|
onValueChange = { value ->
|
||||||
|
viewModel.onPrivateKeyChange(value)
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.focusRequester(FocusRequester.Default),
|
||||||
|
onClick = {
|
||||||
|
viewModel.generateKeyPair()
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Refresh,
|
||||||
|
stringResource(R.string.rotate_keys),
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label = { Text(stringResource(R.string.private_key)) },
|
||||||
|
singleLine = true,
|
||||||
|
placeholder = { Text(stringResource(R.string.base64_key)) },
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = baseTextBoxModifier.fillMaxWidth().focusRequester(FocusRequester.Default),
|
||||||
|
value = proxyInterface.publicKey,
|
||||||
|
enabled = false,
|
||||||
|
onValueChange = {},
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.focusRequester(FocusRequester.Default),
|
||||||
|
onClick = {
|
||||||
|
clipboardManager.setText(AnnotatedString(proxyInterface.publicKey))
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.ContentCopy,
|
||||||
|
stringResource(R.string.copy_public_key),
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label = { Text(stringResource(R.string.public_key)) },
|
||||||
|
singleLine = true,
|
||||||
|
placeholder = { Text(stringResource(R.string.base64_key)) },
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions
|
||||||
|
)
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = proxyInterface.addresses,
|
||||||
|
onValueChange = { value ->
|
||||||
|
viewModel.onAddressesChanged(value)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.addresses),
|
||||||
|
hint = stringResource(R.string.comma_separated_list),
|
||||||
|
modifier = baseTextBoxModifier
|
||||||
|
.fillMaxWidth(3 / 5f)
|
||||||
|
.padding(end = 5.dp)
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = proxyInterface.listenPort,
|
||||||
|
onValueChange = { value -> viewModel.onListenPortChanged(value) },
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.listen_port),
|
||||||
|
hint = stringResource(R.string.random),
|
||||||
|
modifier = baseTextBoxModifier.width(IntrinsicSize.Min)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = proxyInterface.dnsServers,
|
||||||
|
onValueChange = { value -> viewModel.onDnsServersChanged(value) },
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.dns_servers),
|
||||||
|
hint = stringResource(R.string.comma_separated_list),
|
||||||
|
modifier = baseTextBoxModifier
|
||||||
|
.fillMaxWidth(3 / 5f)
|
||||||
|
.padding(end = 5.dp)
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = proxyInterface.mtu,
|
||||||
|
onValueChange = { value -> viewModel.onMtuChanged(value) },
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.mtu),
|
||||||
|
hint = stringResource(R.string.auto),
|
||||||
|
modifier = baseTextBoxModifier.width(IntrinsicSize.Min)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = 5.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
showApplicationsDialog = true
|
||||||
|
}) {
|
||||||
|
Text(applicationButtonText())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proxyPeers.forEachIndexed { index, peer ->
|
||||||
|
Surface(
|
||||||
|
tonalElevation = 2.dp,
|
||||||
|
shadowElevation = 2.dp,
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
modifier = (if (WireGuardAutoTunnel.isRunningOnAndroidTv(context))
|
||||||
|
Modifier
|
||||||
|
.fillMaxHeight(fillMaxHeight)
|
||||||
|
.fillMaxWidth(fillMaxWidth)
|
||||||
|
else Modifier.fillMaxWidth(fillMaxWidth)).padding(
|
||||||
|
top = 10.dp,
|
||||||
|
bottom = 10.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 15.dp)
|
||||||
|
.padding(bottom = 10.dp)
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 5.dp)) {
|
modifier = Modifier
|
||||||
SectionTitle(
|
.fillMaxWidth()
|
||||||
stringResource(R.string.peer), padding = screenPadding)
|
.padding(horizontal = 5.dp)
|
||||||
IconButton(onClick = { viewModel.onDeletePeer(index) }) {
|
) {
|
||||||
|
SectionTitle(stringResource(R.string.peer), padding = screenPadding)
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.onDeletePeer(index)
|
||||||
|
}
|
||||||
|
) {
|
||||||
Icon(Icons.Rounded.Delete, stringResource(R.string.delete))
|
Icon(Icons.Rounded.Delete, stringResource(R.string.delete))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = peer.publicKey,
|
value = peer.publicKey,
|
||||||
onValueChange = { value ->
|
onValueChange = { value ->
|
||||||
viewModel.onPeerPublicKeyChange(index, value)
|
viewModel.onPeerPublicKeyChange(
|
||||||
|
index,
|
||||||
|
value
|
||||||
|
)
|
||||||
},
|
},
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.public_key),
|
label = stringResource(R.string.public_key),
|
||||||
hint = stringResource(R.string.base64_key),
|
hint = stringResource(R.string.base64_key),
|
||||||
modifier = Modifier.fillMaxWidth())
|
modifier = baseTextBoxModifier.fillMaxWidth()
|
||||||
|
)
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = peer.preSharedKey,
|
value = peer.preSharedKey,
|
||||||
onValueChange = { value ->
|
onValueChange = { value ->
|
||||||
viewModel.onPreSharedKeyChange(index, value)
|
viewModel.onPreSharedKeyChange(
|
||||||
|
index,
|
||||||
|
value
|
||||||
|
)
|
||||||
},
|
},
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.preshared_key),
|
label = stringResource(R.string.preshared_key),
|
||||||
hint = stringResource(R.string.optional),
|
hint = stringResource(R.string.optional),
|
||||||
modifier = Modifier.fillMaxWidth())
|
modifier = baseTextBoxModifier.fillMaxWidth()
|
||||||
|
)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = baseTextBoxModifier.fillMaxWidth(),
|
||||||
value = peer.persistentKeepalive,
|
value = peer.persistentKeepalive,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
onValueChange = { value ->
|
onValueChange = { value ->
|
||||||
viewModel.onPersistentKeepaliveChanged(index, value)
|
viewModel.onPersistentKeepaliveChanged(index, value)
|
||||||
},
|
|
||||||
trailingIcon = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.seconds),
|
|
||||||
modifier = Modifier.padding(end = 10.dp))
|
|
||||||
},
|
},
|
||||||
|
trailingIcon = { Text(stringResource(R.string.seconds), modifier = Modifier.padding(end = 10.dp)) },
|
||||||
label = { Text(stringResource(R.string.persistent_keepalive)) },
|
label = { Text(stringResource(R.string.persistent_keepalive)) },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
placeholder = {
|
placeholder = { Text(stringResource(R.string.optional_no_recommend)) },
|
||||||
Text(stringResource(R.string.optional_no_recommend))
|
|
||||||
},
|
|
||||||
keyboardOptions = keyboardOptions,
|
keyboardOptions = keyboardOptions,
|
||||||
keyboardActions = keyboardActions)
|
keyboardActions = keyboardActions
|
||||||
|
)
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = peer.endpoint,
|
value = peer.endpoint,
|
||||||
onValueChange = { value ->
|
onValueChange = { value ->
|
||||||
viewModel.onEndpointChange(index, value)
|
viewModel.onEndpointChange(
|
||||||
|
index,
|
||||||
|
value
|
||||||
|
)
|
||||||
},
|
},
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.endpoint),
|
label = stringResource(R.string.endpoint),
|
||||||
hint = stringResource(R.string.endpoint).lowercase(),
|
hint = stringResource(R.string.endpoint).lowercase(),
|
||||||
modifier = Modifier.fillMaxWidth())
|
modifier = baseTextBoxModifier.fillMaxWidth()
|
||||||
|
)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = baseTextBoxModifier.fillMaxWidth(),
|
||||||
value = peer.allowedIps,
|
value = peer.allowedIps,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
onValueChange = { value ->
|
onValueChange = { value ->
|
||||||
viewModel.onAllowedIpsChange(index, value)
|
viewModel.onAllowedIpsChange(
|
||||||
|
index,
|
||||||
|
value
|
||||||
|
)
|
||||||
},
|
},
|
||||||
label = { Text(stringResource(R.string.allowed_ips)) },
|
label = { Text(stringResource(R.string.allowed_ips)) },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
placeholder = {
|
placeholder = { Text(stringResource(R.string.comma_separated_list)) },
|
||||||
Text(stringResource(R.string.comma_separated_list))
|
|
||||||
},
|
|
||||||
keyboardOptions = keyboardOptions,
|
keyboardOptions = keyboardOptions,
|
||||||
keyboardActions = keyboardActions)
|
keyboardActions = keyboardActions
|
||||||
}
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxSize().padding(bottom = 140.dp)) {
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(bottom = 140.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Center) {
|
horizontalArrangement = Arrangement.Center
|
||||||
TextButton(onClick = { viewModel.addEmptyPeer() }) {
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.addEmptyPeer()
|
||||||
|
}) {
|
||||||
Text(stringResource(R.string.add_peer))
|
Text(stringResource(R.string.add_peer))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
|
Spacer(modifier = Modifier.weight(.17f))
|
||||||
}
|
}
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
|
||||||
Spacer(modifier = Modifier.weight(.17f))
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
-18
@@ -1,18 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.config
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Packages
|
|
||||||
|
|
||||||
data class ConfigUiState(
|
|
||||||
val proxyPeers: List<PeerProxy> = arrayListOf(PeerProxy()),
|
|
||||||
val interfaceProxy: InterfaceProxy = InterfaceProxy(),
|
|
||||||
val packages: Packages = emptyList(),
|
|
||||||
val checkedPackageNames: List<String> = emptyList(),
|
|
||||||
val include: Boolean = true,
|
|
||||||
val isAllApplicationsEnabled : Boolean = false,
|
|
||||||
val loading: Boolean = true,
|
|
||||||
val tunnel: TunnelConfig? = null,
|
|
||||||
val tunnelName: String = ""
|
|
||||||
)
|
|
||||||
+355
-272
@@ -5,6 +5,8 @@ import android.app.Application
|
|||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.compose.runtime.toMutableStateList
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
@@ -12,301 +14,382 @@ import com.wireguard.config.Interface
|
|||||||
import com.wireguard.config.Peer
|
import com.wireguard.config.Peer
|
||||||
import com.wireguard.crypto.Key
|
import com.wireguard.crypto.Key
|
||||||
import com.wireguard.crypto.KeyPair
|
import com.wireguard.crypto.KeyPair
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
|
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
|
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
||||||
import com.zaneschepke.wireguardautotunnel.util.removeAt
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.update
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ConfigViewModel
|
class ConfigViewModel @Inject constructor(private val application : Application,
|
||||||
@Inject
|
private val tunnelRepo : TunnelConfigDao,
|
||||||
constructor(
|
private val settingsRepo : SettingsDoa
|
||||||
private val application: Application,
|
|
||||||
private val tunnelConfigRepository: TunnelConfigRepository,
|
|
||||||
private val settingsRepository: SettingsRepository,
|
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val packageManager = application.packageManager
|
private val _tunnel = MutableStateFlow<TunnelConfig?>(null)
|
||||||
|
private val _tunnelName = MutableStateFlow("")
|
||||||
|
val tunnelName get() = _tunnelName.asStateFlow()
|
||||||
|
val tunnel get() = _tunnel.asStateFlow()
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(ConfigUiState())
|
private var _proxyPeers = MutableStateFlow(mutableStateListOf<PeerProxy>())
|
||||||
val uiState = _uiState.asStateFlow()
|
val proxyPeers get() = _proxyPeers.asStateFlow()
|
||||||
|
|
||||||
fun init(tunnelId : String) = viewModelScope.launch(Dispatchers.IO) {
|
private var _interface = MutableStateFlow(InterfaceProxy())
|
||||||
val packages = getQueriedPackages("")
|
val interfaceProxy = _interface.asStateFlow()
|
||||||
val state = if(tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
|
||||||
val tunnelConfig =
|
private val _packages = MutableStateFlow(emptyList<PackageInfo>())
|
||||||
tunnelConfigRepository.getAll().firstOrNull { it.id.toString() == tunnelId }
|
val packages get() = _packages.asStateFlow()
|
||||||
if (tunnelConfig != null) {
|
private val packageManager = application.packageManager
|
||||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
|
||||||
val proxyPeers = config.peers.map { PeerProxy.from(it) }
|
private val _checkedPackages = MutableStateFlow(mutableStateListOf<String>())
|
||||||
val proxyInterface = InterfaceProxy.from(config.`interface`)
|
val checkedPackages get() = _checkedPackages.asStateFlow()
|
||||||
var include = true
|
private val _include = MutableStateFlow(true)
|
||||||
var isAllApplicationsEnabled = false
|
val include get() = _include.asStateFlow()
|
||||||
val checkedPackages =
|
|
||||||
if (config.`interface`.includedApplications.isNotEmpty()) {
|
private val _isAllApplicationsEnabled = MutableStateFlow(false)
|
||||||
config.`interface`.includedApplications
|
val isAllApplicationsEnabled get() = _isAllApplicationsEnabled.asStateFlow()
|
||||||
} else if (config.`interface`.excludedApplications.isNotEmpty()) {
|
private val _isDefaultTunnel = MutableStateFlow(false)
|
||||||
include = false
|
val isDefaultTunnel = _isDefaultTunnel.asStateFlow()
|
||||||
config.`interface`.excludedApplications
|
|
||||||
} else {
|
private lateinit var tunnelConfig: TunnelConfig
|
||||||
isAllApplicationsEnabled = true
|
|
||||||
emptySet()
|
suspend fun onScreenLoad(id : String) {
|
||||||
}
|
if(id != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
||||||
ConfigUiState(
|
tunnelConfig = getTunnelConfigById(id) ?: throw WgTunnelException("Config not found")
|
||||||
proxyPeers,
|
emitScreenData()
|
||||||
proxyInterface,
|
|
||||||
packages,
|
|
||||||
checkedPackages.toList(),
|
|
||||||
include,
|
|
||||||
isAllApplicationsEnabled,
|
|
||||||
false,
|
|
||||||
tunnelConfig,
|
|
||||||
tunnelConfig.name)
|
|
||||||
} else {
|
|
||||||
ConfigUiState(loading = false, packages = packages)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
ConfigUiState(loading = false, packages = packages)
|
emitEmptyScreenData()
|
||||||
}
|
}
|
||||||
_uiState.value = state
|
|
||||||
}
|
}
|
||||||
fun onTunnelNameChange(name: String) {
|
|
||||||
_uiState.value = _uiState.value.copy(tunnelName = name)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onIncludeChange(include: Boolean) {
|
private fun emitEmptyScreenData() {
|
||||||
_uiState.value = _uiState.value.copy(include = include)
|
tunnelConfig = TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = "")
|
||||||
}
|
viewModelScope.launch {
|
||||||
|
emitTunnelConfig()
|
||||||
fun onAddCheckedPackage(packageName: String) {
|
emitPeerProxy(PeerProxy())
|
||||||
_uiState.value =
|
emitInterfaceProxy(InterfaceProxy())
|
||||||
_uiState.value.copy(checkedPackageNames = _uiState.value.checkedPackageNames + packageName)
|
emitTunnelConfigName()
|
||||||
}
|
emitDefaultTunnelStatus()
|
||||||
|
emitQueriedPackages("")
|
||||||
fun onAllApplicationsChange(isAllApplicationsEnabled: Boolean) {
|
emitTunnelAllApplicationsEnabled()
|
||||||
_uiState.value = _uiState.value.copy(isAllApplicationsEnabled = isAllApplicationsEnabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onRemoveCheckedPackage(packageName: String) {
|
|
||||||
_uiState.value =
|
|
||||||
_uiState.value.copy(checkedPackageNames = _uiState.value.checkedPackageNames - packageName)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getQueriedPackages(query: String): List<PackageInfo> {
|
|
||||||
return getAllInternetCapablePackages().filter {
|
|
||||||
getPackageLabel(it).lowercase().contains(query.lowercase())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPackageLabel(packageInfo: PackageInfo): String {
|
|
||||||
return packageInfo.applicationInfo.loadLabel(application.packageManager).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getAllInternetCapablePackages(): List<PackageInfo> {
|
|
||||||
return getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getPackagesHoldingPermissions(permissions: Array<String>): List<PackageInfo> {
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
packageManager.getPackagesHoldingPermissions(
|
|
||||||
permissions, PackageManager.PackageInfoFlags.of(0L))
|
|
||||||
} else {
|
|
||||||
packageManager.getPackagesHoldingPermissions(permissions, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isAllApplicationsEnabled(): Boolean {
|
|
||||||
return _uiState.value.isAllApplicationsEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveConfig(tunnelConfig: TunnelConfig) =
|
|
||||||
viewModelScope.launch {
|
|
||||||
tunnelConfigRepository.save(tunnelConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) =
|
|
||||||
viewModelScope.launch {
|
|
||||||
if (tunnelConfig != null) {
|
|
||||||
saveConfig(tunnelConfig).join()
|
|
||||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
|
||||||
updateSettingsDefaultTunnel(tunnelConfig)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun updateSettingsDefaultTunnel(tunnelConfig: TunnelConfig) {
|
|
||||||
val settings = settingsRepository.getSettingsFlow().first()
|
|
||||||
if (settings.defaultTunnel != null) {
|
|
||||||
if (tunnelConfig.id == TunnelConfig.from(settings.defaultTunnel!!).id) {
|
|
||||||
settingsRepository.save(settings.copy(defaultTunnel = tunnelConfig.toString()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildPeerListFromProxyPeers(): List<Peer> {
|
|
||||||
return _uiState.value.proxyPeers.map {
|
private suspend fun emitScreenData() {
|
||||||
val builder = Peer.Builder()
|
emitTunnelConfig()
|
||||||
if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim())
|
emitPeersFromConfig()
|
||||||
if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim())
|
emitInterfaceFromConfig()
|
||||||
if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.trim())
|
emitTunnelConfigName()
|
||||||
if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.trim())
|
emitDefaultTunnelStatus()
|
||||||
if (it.persistentKeepalive.isNotEmpty()) {
|
emitQueriedPackages("")
|
||||||
builder.parsePersistentKeepalive(it.persistentKeepalive.trim())
|
emitCurrentPackageConfigurations()
|
||||||
}
|
|
||||||
builder.build()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun emptyCheckedPackagesList() {
|
private suspend fun emitDefaultTunnelStatus() {
|
||||||
_uiState.value = _uiState.value.copy(checkedPackageNames = emptyList())
|
val settings = settingsRepo.getAll()
|
||||||
}
|
if(settings.isNotEmpty()) {
|
||||||
|
_isDefaultTunnel.value = settings.first().isTunnelConfigDefault(tunnelConfig)
|
||||||
private fun buildInterfaceListFromProxyInterface(): Interface {
|
|
||||||
val builder = Interface.Builder()
|
|
||||||
builder.parsePrivateKey(_uiState.value.interfaceProxy.privateKey.trim())
|
|
||||||
builder.parseAddresses(_uiState.value.interfaceProxy.addresses.trim())
|
|
||||||
builder.parseDnsServers(_uiState.value.interfaceProxy.dnsServers.trim())
|
|
||||||
if (_uiState.value.interfaceProxy.mtu.isNotEmpty())
|
|
||||||
builder.parseMtu(_uiState.value.interfaceProxy.mtu.trim())
|
|
||||||
if (_uiState.value.interfaceProxy.listenPort.isNotEmpty()) {
|
|
||||||
builder.parseListenPort(_uiState.value.interfaceProxy.listenPort.trim())
|
|
||||||
}
|
|
||||||
if (isAllApplicationsEnabled()) emptyCheckedPackagesList()
|
|
||||||
if (_uiState.value.include) builder.includeApplications(_uiState.value.checkedPackageNames)
|
|
||||||
if (!_uiState.value.include) builder.excludeApplications(_uiState.value.checkedPackageNames)
|
|
||||||
return builder.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onSaveAllChanges(): Result<Event> {
|
|
||||||
return try {
|
|
||||||
val peerList = buildPeerListFromProxyPeers()
|
|
||||||
val wgInterface = buildInterfaceListFromProxyInterface()
|
|
||||||
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
|
||||||
val tunnelConfig =
|
|
||||||
_uiState.value.tunnel?.copy(
|
|
||||||
name = _uiState.value.tunnelName, wgQuick = config.toWgQuickString())
|
|
||||||
updateTunnelConfig(tunnelConfig)
|
|
||||||
Result.Success(Event.Message.ConfigSaved)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Result.Error(Event.Error.Exception(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onPeerPublicKeyChange(index: Int, value: String) {
|
|
||||||
_uiState.value =
|
|
||||||
_uiState.value.copy(
|
|
||||||
proxyPeers =
|
|
||||||
_uiState.value.proxyPeers.update(
|
|
||||||
index, _uiState.value.proxyPeers[index].copy(publicKey = value)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onPreSharedKeyChange(index: Int, value: String) {
|
|
||||||
_uiState.value =
|
|
||||||
_uiState.value.copy(
|
|
||||||
proxyPeers =
|
|
||||||
_uiState.value.proxyPeers.update(
|
|
||||||
index, _uiState.value.proxyPeers[index].copy(preSharedKey = value)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onEndpointChange(index: Int, value: String) {
|
|
||||||
_uiState.value =
|
|
||||||
_uiState.value.copy(
|
|
||||||
proxyPeers =
|
|
||||||
_uiState.value.proxyPeers.update(
|
|
||||||
index, _uiState.value.proxyPeers[index].copy(endpoint = value)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onAllowedIpsChange(index: Int, value: String) {
|
|
||||||
_uiState.value =
|
|
||||||
_uiState.value.copy(
|
|
||||||
proxyPeers =
|
|
||||||
_uiState.value.proxyPeers.update(
|
|
||||||
index, _uiState.value.proxyPeers[index].copy(allowedIps = value)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onPersistentKeepaliveChanged(index: Int, value: String) {
|
|
||||||
_uiState.value =
|
|
||||||
_uiState.value.copy(
|
|
||||||
proxyPeers =
|
|
||||||
_uiState.value.proxyPeers.update(
|
|
||||||
index, _uiState.value.proxyPeers[index].copy(persistentKeepalive = value)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onDeletePeer(index: Int) {
|
|
||||||
_uiState.value = _uiState.value.copy(
|
|
||||||
proxyPeers = _uiState.value.proxyPeers.removeAt(index)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addEmptyPeer() {
|
|
||||||
_uiState.value = _uiState.value.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generateKeyPair() {
|
|
||||||
val keyPair = KeyPair()
|
|
||||||
_uiState.value =
|
|
||||||
_uiState.value.copy(
|
|
||||||
interfaceProxy =
|
|
||||||
_uiState.value.interfaceProxy.copy(
|
|
||||||
privateKey = keyPair.privateKey.toBase64(),
|
|
||||||
publicKey = keyPair.publicKey.toBase64()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onAddressesChanged(value: String) {
|
|
||||||
_uiState.value =
|
|
||||||
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onListenPortChanged(value: String) {
|
|
||||||
_uiState.value =
|
|
||||||
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onDnsServersChanged(value: String) {
|
|
||||||
_uiState.value =
|
|
||||||
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onMtuChanged(value: String) {
|
|
||||||
_uiState.value =
|
|
||||||
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onInterfacePublicKeyChange(value: String) {
|
|
||||||
_uiState.value =
|
|
||||||
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onPrivateKeyChange(value: String) {
|
|
||||||
_uiState.value =
|
|
||||||
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value))
|
|
||||||
if (NumberUtils.isValidKey(value)) {
|
|
||||||
val pair = KeyPair(Key.fromBase64(value))
|
|
||||||
onInterfacePublicKeyChange(pair.publicKey.toBase64())
|
|
||||||
} else {
|
|
||||||
onInterfacePublicKeyChange("")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun emitQueriedPackages(query: String) {
|
|
||||||
val packages =
|
|
||||||
getAllInternetCapablePackages().filter {
|
|
||||||
getPackageLabel(it).lowercase().contains(query.lowercase())
|
|
||||||
}
|
}
|
||||||
_uiState.value = _uiState.value.copy(packages = packages)
|
}
|
||||||
}
|
|
||||||
|
private fun emitInterfaceFromConfig() {
|
||||||
|
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||||
|
_interface.value = InterfaceProxy.from(config.`interface`)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emitPeersFromConfig() {
|
||||||
|
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||||
|
config.peers.forEach{
|
||||||
|
_proxyPeers.value.add(PeerProxy.from(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emitPeerProxy(peerProxy: PeerProxy) {
|
||||||
|
_proxyPeers.value.add(peerProxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emitInterfaceProxy(interfaceProxy: InterfaceProxy) {
|
||||||
|
_interface.value = interfaceProxy
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getTunnelConfigById(id : String) : TunnelConfig? {
|
||||||
|
return try {
|
||||||
|
tunnelRepo.getById(id.toLong())
|
||||||
|
} catch (_ : Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun emitTunnelConfig() {
|
||||||
|
_tunnel.emit(tunnelConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun emitTunnelConfigName() {
|
||||||
|
_tunnelName.emit(tunnelConfig.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onTunnelNameChange(name : String) {
|
||||||
|
_tunnelName.value = name
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onIncludeChange(include : Boolean) {
|
||||||
|
_include.value = include
|
||||||
|
}
|
||||||
|
fun onAddCheckedPackage(packageName : String) {
|
||||||
|
_checkedPackages.value.add(packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAllApplicationsChange(isAllApplicationsEnabled : Boolean) {
|
||||||
|
_isAllApplicationsEnabled.value = isAllApplicationsEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onRemoveCheckedPackage(packageName : String) {
|
||||||
|
_checkedPackages.value.remove(packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun emitSplitTunnelConfiguration(config : Config) {
|
||||||
|
val excludedApps = config.`interface`.excludedApplications
|
||||||
|
val includedApps = config.`interface`.includedApplications
|
||||||
|
if (excludedApps.isNotEmpty() || includedApps.isNotEmpty()) {
|
||||||
|
emitTunnelAllApplicationsDisabled()
|
||||||
|
determineAppInclusionState(excludedApps, includedApps)
|
||||||
|
} else {
|
||||||
|
emitTunnelAllApplicationsEnabled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun determineAppInclusionState(excludedApps : Set<String>, includedApps : Set<String>) {
|
||||||
|
if (excludedApps.isEmpty()) {
|
||||||
|
emitIncludedAppsExist()
|
||||||
|
emitCheckedApps(includedApps)
|
||||||
|
} else {
|
||||||
|
emitExcludedAppsExist()
|
||||||
|
emitCheckedApps(excludedApps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun emitIncludedAppsExist() {
|
||||||
|
_include.emit(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun emitExcludedAppsExist() {
|
||||||
|
_include.emit(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun emitCheckedApps(apps : Set<String>) {
|
||||||
|
_checkedPackages.emit(apps.toMutableStateList())
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun emitTunnelAllApplicationsEnabled() {
|
||||||
|
_isAllApplicationsEnabled.emit(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun emitTunnelAllApplicationsDisabled() {
|
||||||
|
_isAllApplicationsEnabled.emit(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emitCurrentPackageConfigurations() {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||||
|
emitSplitTunnelConfiguration(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun emitQueriedPackages(query : String) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val packages = getAllInternetCapablePackages().filter {
|
||||||
|
getPackageLabel(it).lowercase().contains(query.lowercase())
|
||||||
|
}
|
||||||
|
_packages.emit(packages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPackageLabel(packageInfo : PackageInfo) : String {
|
||||||
|
return packageInfo.applicationInfo.loadLabel(application.packageManager).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun getAllInternetCapablePackages() : List<PackageInfo> {
|
||||||
|
return getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPackagesHoldingPermissions(permissions: Array<String>): List<PackageInfo> {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
packageManager.getPackagesHoldingPermissions(permissions, PackageManager.PackageInfoFlags.of(0L))
|
||||||
|
} else {
|
||||||
|
packageManager.getPackagesHoldingPermissions(permissions, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isAllApplicationsEnabled() : Boolean {
|
||||||
|
return _isAllApplicationsEnabled.value
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isIncludeApplicationsEnabled() : Boolean {
|
||||||
|
return _include.value
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun saveConfig(tunnelConfig: TunnelConfig) {
|
||||||
|
tunnelRepo.save(tunnelConfig)
|
||||||
|
}
|
||||||
|
private suspend fun updateTunnelConfig(tunnelConfig: TunnelConfig?) {
|
||||||
|
if(tunnelConfig != null) {
|
||||||
|
saveConfig(tunnelConfig)
|
||||||
|
updateSettingsDefaultTunnel(tunnelConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun updateSettingsDefaultTunnel(tunnelConfig: TunnelConfig) {
|
||||||
|
val settings = settingsRepo.getAll()
|
||||||
|
if(settings.isNotEmpty()) {
|
||||||
|
val setting = settings[0]
|
||||||
|
if(setting.defaultTunnel != null) {
|
||||||
|
if(tunnelConfig.id == TunnelConfig.from(setting.defaultTunnel!!).id) {
|
||||||
|
settingsRepo.save(setting.copy(
|
||||||
|
defaultTunnel = tunnelConfig.toString()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildPeerListFromProxyPeers() : List<Peer> {
|
||||||
|
return _proxyPeers.value.map {
|
||||||
|
val builder = Peer.Builder()
|
||||||
|
if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim())
|
||||||
|
if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim())
|
||||||
|
if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.trim())
|
||||||
|
if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.trim())
|
||||||
|
if (it.persistentKeepalive.isNotEmpty()) builder.parsePersistentKeepalive(it.persistentKeepalive.trim())
|
||||||
|
builder.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildInterfaceListFromProxyInterface() : Interface {
|
||||||
|
val builder = Interface.Builder()
|
||||||
|
builder.parsePrivateKey(_interface.value.privateKey.trim())
|
||||||
|
builder.parseAddresses(_interface.value.addresses.trim())
|
||||||
|
builder.parseDnsServers(_interface.value.dnsServers.trim())
|
||||||
|
if(_interface.value.mtu.isNotEmpty()) builder.parseMtu(_interface.value.mtu.trim())
|
||||||
|
if(_interface.value.listenPort.isNotEmpty()) builder.parseListenPort(_interface.value.listenPort.trim())
|
||||||
|
if(isAllApplicationsEnabled()) _checkedPackages.value.clear()
|
||||||
|
if(_include.value) builder.includeApplications(_checkedPackages.value)
|
||||||
|
if(!_include.value) builder.excludeApplications(_checkedPackages.value)
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun onSaveAllChanges() {
|
||||||
|
try {
|
||||||
|
val peerList = buildPeerListFromProxyPeers()
|
||||||
|
val wgInterface = buildInterfaceListFromProxyInterface()
|
||||||
|
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
||||||
|
val tunnelConfig = _tunnel.value?.copy(
|
||||||
|
name = _tunnelName.value,
|
||||||
|
wgQuick = config.toWgQuickString()
|
||||||
|
)
|
||||||
|
updateTunnelConfig(tunnelConfig)
|
||||||
|
} catch (e : Exception) {
|
||||||
|
throw WgTunnelException("Error: ${e.cause?.message?.lowercase() ?: "unknown error occurred"}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPeerPublicKeyChange(index: Int, publicKey: String) {
|
||||||
|
_proxyPeers.value[index] = _proxyPeers.value[index].copy(
|
||||||
|
publicKey = publicKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPreSharedKeyChange(index: Int, value: String) {
|
||||||
|
_proxyPeers.value[index] = _proxyPeers.value[index].copy(
|
||||||
|
preSharedKey = value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEndpointChange(index: Int, value: String) {
|
||||||
|
_proxyPeers.value[index] = _proxyPeers.value[index].copy(
|
||||||
|
endpoint = value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAllowedIpsChange(index: Int, value: String) {
|
||||||
|
_proxyPeers.value[index] = _proxyPeers.value[index].copy(
|
||||||
|
allowedIps = value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPersistentKeepaliveChanged(index : Int, value : String) {
|
||||||
|
_proxyPeers.value[index] = _proxyPeers.value[index].copy(
|
||||||
|
persistentKeepalive = value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDeletePeer(index: Int) {
|
||||||
|
proxyPeers.value.removeAt(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addEmptyPeer() {
|
||||||
|
_proxyPeers.value.add(PeerProxy())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateKeyPair() {
|
||||||
|
val keyPair = KeyPair()
|
||||||
|
_interface.value = _interface.value.copy(
|
||||||
|
privateKey = keyPair.privateKey.toBase64(),
|
||||||
|
publicKey = keyPair.publicKey.toBase64()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAddressesChanged(value: String) {
|
||||||
|
_interface.value = _interface.value.copy(
|
||||||
|
addresses = value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onListenPortChanged(value: String) {
|
||||||
|
_interface.value = _interface.value.copy(
|
||||||
|
listenPort = value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDnsServersChanged(value: String) {
|
||||||
|
_interface.value = _interface.value.copy(
|
||||||
|
dnsServers = value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onMtuChanged(value: String) {
|
||||||
|
_interface.value = _interface.value.copy(
|
||||||
|
mtu = value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onInterfacePublicKeyChange(value : String) {
|
||||||
|
_interface.value = _interface.value.copy(
|
||||||
|
publicKey = value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPrivateKeyChange(value: String) {
|
||||||
|
_interface.value = _interface.value.copy(
|
||||||
|
privateKey = value
|
||||||
|
)
|
||||||
|
if(NumberUtils.isValidKey(value)) {
|
||||||
|
val pair = KeyPair(Key.fromBase64(value))
|
||||||
|
onInterfacePublicKeyChange(pair.publicKey.toBase64())
|
||||||
|
} else {
|
||||||
|
onInterfacePublicKeyChange("")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+369
-414
@@ -8,36 +8,27 @@ import android.os.Build
|
|||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
import androidx.compose.foundation.gestures.ScrollableDefaults
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.requiredWidth
|
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.overscroll
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Create
|
import androidx.compose.material.icons.filled.Create
|
||||||
import androidx.compose.material.icons.filled.FileOpen
|
import androidx.compose.material.icons.filled.FileOpen
|
||||||
import androidx.compose.material.icons.filled.QrCode
|
import androidx.compose.material.icons.filled.QrCode
|
||||||
import androidx.compose.material.icons.rounded.Add
|
import androidx.compose.material.icons.rounded.Add
|
||||||
import androidx.compose.material.icons.rounded.Bolt
|
|
||||||
import androidx.compose.material.icons.rounded.Circle
|
import androidx.compose.material.icons.rounded.Circle
|
||||||
import androidx.compose.material.icons.rounded.Delete
|
import androidx.compose.material.icons.rounded.Delete
|
||||||
import androidx.compose.material.icons.rounded.Edit
|
import androidx.compose.material.icons.rounded.Edit
|
||||||
@@ -51,18 +42,14 @@ import androidx.compose.material3.FloatingActionButton
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.MaterialTheme.typography
|
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -74,10 +61,13 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -89,474 +79,439 @@ import androidx.navigation.NavController
|
|||||||
import com.journeyapps.barcodescanner.ScanContract
|
import com.journeyapps.barcodescanner.ScanContract
|
||||||
import com.journeyapps.barcodescanner.ScanOptions
|
import com.journeyapps.barcodescanner.ScanOptions
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
|
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
import com.zaneschepke.wireguardautotunnel.ui.Routes
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
|
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
import com.zaneschepke.wireguardautotunnel.ui.theme.brickRed
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.corn
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.mint
|
import com.zaneschepke.wireguardautotunnel.ui.theme.mint
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(
|
fun MainScreen(
|
||||||
viewModel: MainViewModel = hiltViewModel(),
|
viewModel: MainViewModel = hiltViewModel(),
|
||||||
padding: PaddingValues,
|
padding: PaddingValues,
|
||||||
focusRequester: FocusRequester,
|
|
||||||
showSnackbarMessage: (String) -> Unit,
|
showSnackbarMessage: (String) -> Unit,
|
||||||
navController: NavController
|
navController: NavController
|
||||||
) {
|
) {
|
||||||
val haptic = LocalHapticFeedback.current
|
|
||||||
val context = LocalContext.current
|
|
||||||
val isVisible = rememberSaveable { mutableStateOf(true) }
|
|
||||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
|
||||||
|
|
||||||
val sheetState = rememberModalBottomSheetState()
|
val haptic = LocalHapticFeedback.current
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
val context = LocalContext.current
|
||||||
|
val isVisible = rememberSaveable { mutableStateOf(true) }
|
||||||
|
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||||
|
|
||||||
var showPrimaryChangeAlertDialog by remember { mutableStateOf(false) }
|
val sheetState = rememberModalBottomSheetState()
|
||||||
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
var showPrimaryChangeAlertDialog by remember { mutableStateOf(false) }
|
||||||
|
val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf())
|
||||||
|
val handshakeStatus by viewModel.handshakeStatus.collectAsStateWithLifecycle(HandshakeStatus.NOT_STARTED)
|
||||||
|
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
||||||
|
val state by viewModel.state.collectAsStateWithLifecycle(Tunnel.State.DOWN)
|
||||||
|
val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle("")
|
||||||
|
val settings by viewModel.settings.collectAsStateWithLifecycle()
|
||||||
|
val statistics by viewModel.statistics.collectAsStateWithLifecycle(null)
|
||||||
|
|
||||||
LaunchedEffect(uiState.loading) {
|
// Nested scroll for control FAB
|
||||||
if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
val nestedScrollConnection = remember {
|
||||||
delay(Constants.FOCUS_REQUEST_DELAY)
|
object : NestedScrollConnection {
|
||||||
focusRequester.requestFocus()
|
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||||
|
// Hide FAB
|
||||||
|
if (available.y < -1) {
|
||||||
|
isVisible.value = false
|
||||||
|
}
|
||||||
|
// Show FAB
|
||||||
|
if (available.y > 1) {
|
||||||
|
isVisible.value = true
|
||||||
|
}
|
||||||
|
return Offset.Zero
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (uiState.loading) {
|
val tunnelFileImportResultLauncher = rememberLauncherForActivityResult(object : ActivityResultContracts.GetContent() {
|
||||||
LoadingScreen()
|
override fun createIntent(context: Context, input: String): Intent {
|
||||||
return
|
val intent = super.createIntent(context, input)
|
||||||
}
|
|
||||||
|
|
||||||
val tunnelFileImportResultLauncher =
|
/* AndroidTV now comes with stubs that do nothing but display a Toast less helpful than
|
||||||
rememberLauncherForActivityResult(
|
* what we can do, so detect this and throw an exception that we can catch later. */
|
||||||
object : ActivityResultContracts.GetContent() {
|
val activitiesToResolveIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
override fun createIntent(context: Context, input: String): Intent {
|
context.packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()))
|
||||||
val intent = super.createIntent(context, input)
|
} else {
|
||||||
|
context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
|
||||||
/* AndroidTV now comes with stubs that do nothing but display a Toast less helpful than
|
|
||||||
* what we can do, so detect this and throw an exception that we can catch later. */
|
|
||||||
val activitiesToResolveIntent =
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
context.packageManager.queryIntentActivities(
|
|
||||||
intent,
|
|
||||||
PackageManager.ResolveInfoFlags.of(
|
|
||||||
PackageManager.MATCH_DEFAULT_ONLY.toLong()))
|
|
||||||
} else {
|
|
||||||
context.packageManager.queryIntentActivities(
|
|
||||||
intent, PackageManager.MATCH_DEFAULT_ONLY)
|
|
||||||
}
|
|
||||||
if (activitiesToResolveIntent.all {
|
|
||||||
val name = it.activityInfo.packageName
|
|
||||||
name.startsWith(Constants.GOOGLE_TV_EXPLORER_STUB) ||
|
|
||||||
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
|
|
||||||
}) {
|
|
||||||
showSnackbarMessage(Event.Error.FileExplorerRequired.message)
|
|
||||||
}
|
|
||||||
return intent
|
|
||||||
}
|
}
|
||||||
}) { data ->
|
if (activitiesToResolveIntent.all {
|
||||||
if (data == null) return@rememberLauncherForActivityResult
|
val name = it.activityInfo.packageName
|
||||||
|
name.startsWith(Constants.GOOGLE_TV_EXPLORER_STUB) || name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
|
||||||
|
}) {
|
||||||
|
throw WgTunnelException(context.getString(R.string.no_file_explorer))
|
||||||
|
}
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
}) { data ->
|
||||||
|
if (data == null) return@rememberLauncherForActivityResult
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
viewModel.onTunnelFileSelected(data)
|
||||||
|
} catch (e : WgTunnelException) {
|
||||||
|
showSnackbarMessage(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val scanLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ScanContract(),
|
||||||
|
onResult = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
viewModel.onTunnelFileSelected(data).let {
|
try {
|
||||||
when (it) {
|
viewModel.onTunnelQrResult(it.contents)
|
||||||
is Result.Error -> showSnackbarMessage(it.error.message)
|
} catch (e: Exception) {
|
||||||
is Result.Success -> {}
|
when(e) {
|
||||||
|
is WgTunnelException -> {
|
||||||
|
showSnackbarMessage(e.message)
|
||||||
|
} else -> {
|
||||||
|
showSnackbarMessage("No QR code scanned")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val scanLauncher =
|
)
|
||||||
rememberLauncherForActivityResult(
|
|
||||||
contract = ScanContract(),
|
|
||||||
onResult = {
|
|
||||||
if (it.contents != null) {
|
|
||||||
scope.launch {
|
|
||||||
viewModel.onTunnelQrResult(it.contents).let { result ->
|
|
||||||
when (result) {
|
|
||||||
is Result.Success -> {}
|
|
||||||
is Result.Error -> showSnackbarMessage(result.error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
AnimatedVisibility(showPrimaryChangeAlertDialog) {
|
if(showPrimaryChangeAlertDialog) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { showPrimaryChangeAlertDialog = false },
|
onDismissRequest = {
|
||||||
confirmButton = {
|
showPrimaryChangeAlertDialog = false
|
||||||
TextButton(
|
},
|
||||||
onClick = {
|
confirmButton = {
|
||||||
viewModel.onDefaultTunnelChange(selectedTunnel)
|
TextButton(onClick = {
|
||||||
showPrimaryChangeAlertDialog = false
|
scope.launch {
|
||||||
selectedTunnel = null
|
viewModel.onDefaultTunnelChange(selectedTunnel)
|
||||||
}) {
|
showPrimaryChangeAlertDialog = false
|
||||||
Text(text = stringResource(R.string.okay))
|
selectedTunnel = null
|
||||||
}
|
}
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = { showPrimaryChangeAlertDialog = false }) {
|
|
||||||
Text(text = stringResource(R.string.cancel))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title = { Text(text = stringResource(R.string.primary_tunnel_change)) },
|
|
||||||
text = { Text(text = stringResource(R.string.primary_tunnel_change_question)) })
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
|
|
||||||
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
modifier =
|
|
||||||
Modifier.pointerInput(Unit) {
|
|
||||||
detectTapGestures(
|
|
||||||
onTap = {
|
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) selectedTunnel = null
|
|
||||||
})
|
})
|
||||||
},
|
{ Text(text = stringResource(R.string.okay)) }
|
||||||
floatingActionButtonPosition = FabPosition.End,
|
},
|
||||||
topBar = {
|
dismissButton = {
|
||||||
if (uiState.settings.isAutoTunnelEnabled)
|
TextButton(onClick = {
|
||||||
TopAppBar(
|
showPrimaryChangeAlertDialog = false
|
||||||
title = {
|
})
|
||||||
Row(
|
{ Text(text = stringResource(R.string.cancel)) }
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
},
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
title = { Text(text = stringResource(R.string.primary_tunnel_change)) },
|
||||||
modifier = Modifier.requiredWidth(LocalConfiguration.current.screenWidthDp.dp).padding(end = 5.dp)) {
|
text = { Text(text = stringResource(R.string.primary_tunnel_change_question)) }
|
||||||
Row {
|
)
|
||||||
Icon(
|
}
|
||||||
Icons.Rounded.Bolt,
|
|
||||||
stringResource(id = R.string.auto),
|
fun onTunnelToggle(checked : Boolean , tunnel : TunnelConfig) {
|
||||||
modifier = Modifier.size(25.dp),
|
try {
|
||||||
tint = if(uiState.settings.isAutoTunnelPaused) Color.Gray else mint)
|
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
|
||||||
Text(
|
} catch (e : Exception) {
|
||||||
"Auto-tunneling: ${if(uiState.settings.isAutoTunnelPaused) "paused" else "active" }",
|
showSnackbarMessage(e.message!!)
|
||||||
style = typography.bodyLarge,
|
}
|
||||||
modifier = Modifier.padding(start = 10.dp))
|
}
|
||||||
}
|
|
||||||
if(uiState.settings.isAutoTunnelPaused) TextButton(
|
Scaffold(
|
||||||
onClick = { viewModel.resumeAutoTunneling() },
|
modifier = Modifier.pointerInput(Unit) {
|
||||||
modifier = Modifier.padding(end = 10.dp)) {
|
detectTapGestures(onTap = {
|
||||||
Text("Resume")
|
selectedTunnel = null
|
||||||
} else TextButton(
|
})
|
||||||
onClick = { viewModel.pauseAutoTunneling() },
|
},
|
||||||
modifier = Modifier.padding(end = 10.dp)) {
|
floatingActionButtonPosition = FabPosition.End,
|
||||||
Text("Pause")
|
floatingActionButton = {
|
||||||
}
|
AnimatedVisibility(
|
||||||
}
|
visible = isVisible.value,
|
||||||
},
|
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
||||||
)
|
exit = slideOutVertically(targetOffsetY = { it * 2 }),
|
||||||
},
|
) {
|
||||||
floatingActionButton = {
|
val secondaryColor = MaterialTheme.colorScheme.secondary
|
||||||
AnimatedVisibility(
|
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||||
visible = isVisible.value,
|
var fobColor by remember { mutableStateOf(secondaryColor) }
|
||||||
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
FloatingActionButton(
|
||||||
exit = slideOutVertically(targetOffsetY = { it * 2 })) {
|
modifier = Modifier
|
||||||
val secondaryColor = MaterialTheme.colorScheme.secondary
|
.padding(bottom = 90.dp)
|
||||||
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
.onFocusChanged {
|
||||||
var fobColor by remember { mutableStateOf(secondaryColor) }
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
FloatingActionButton(
|
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||||
modifier =
|
|
||||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
|
||||||
uiState.tunnels.isEmpty())
|
|
||||||
Modifier.focusRequester(focusRequester)
|
|
||||||
else Modifier)
|
|
||||||
.padding(bottom = 90.dp)
|
|
||||||
.onFocusChanged {
|
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
|
||||||
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
onClick = { showBottomSheet = true },
|
,
|
||||||
containerColor = fobColor,
|
onClick = {
|
||||||
shape = RoundedCornerShape(16.dp)) {
|
showBottomSheet = true
|
||||||
|
},
|
||||||
|
containerColor = fobColor,
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.Add,
|
imageVector = Icons.Rounded.Add,
|
||||||
contentDescription = stringResource(id = R.string.add_tunnel),
|
contentDescription = stringResource(id = R.string.add_tunnel),
|
||||||
tint = Color.DarkGray)
|
tint = Color.DarkGray,
|
||||||
}
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}) { innerPadding ->
|
}
|
||||||
AnimatedVisibility(uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) {
|
) {
|
||||||
Column(
|
if (tunnels.isEmpty()) {
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
Column(
|
||||||
verticalArrangement = Arrangement.Center,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier.fillMaxSize().padding(padding)) {
|
verticalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
|
) {
|
||||||
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
|
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (showBottomSheet) {
|
if (showBottomSheet) {
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = { showBottomSheet = false }, sheetState = sheetState) {
|
onDismissRequest = {
|
||||||
|
showBottomSheet = false
|
||||||
|
},
|
||||||
|
sheetState = sheetState
|
||||||
|
) {
|
||||||
// Sheet content
|
// Sheet content
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier = Modifier
|
||||||
Modifier.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable {
|
.clickable {
|
||||||
showBottomSheet = false
|
showBottomSheet = false
|
||||||
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
try {
|
||||||
|
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showSnackbarMessage(e.message!!)
|
||||||
}
|
}
|
||||||
.padding(10.dp)) {
|
}
|
||||||
Icon(
|
.padding(10.dp)
|
||||||
Icons.Filled.FileOpen,
|
) {
|
||||||
contentDescription = stringResource(id = R.string.open_file),
|
Icon(
|
||||||
modifier = Modifier.padding(10.dp))
|
Icons.Filled.FileOpen,
|
||||||
Text(
|
contentDescription = stringResource(id = R.string.open_file),
|
||||||
stringResource(id = R.string.add_tunnels_text),
|
modifier = Modifier.padding(10.dp)
|
||||||
modifier = Modifier.padding(10.dp))
|
)
|
||||||
}
|
Text(
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
stringResource(id = R.string.add_tunnels_text),
|
||||||
Divider()
|
modifier = Modifier.padding(10.dp)
|
||||||
Row(
|
)
|
||||||
modifier =
|
}
|
||||||
Modifier.fillMaxWidth()
|
if(!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
.clickable {
|
Divider()
|
||||||
scope.launch {
|
Row(modifier = Modifier
|
||||||
showBottomSheet = false
|
.fillMaxWidth()
|
||||||
val scanOptions = ScanOptions()
|
.clickable {
|
||||||
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
scope.launch {
|
||||||
scanOptions.setOrientationLocked(true)
|
showBottomSheet = false
|
||||||
scanOptions.setPrompt(context.getString(R.string.scanning_qr))
|
val scanOptions = ScanOptions()
|
||||||
scanOptions.setBeepEnabled(false)
|
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
||||||
scanOptions.captureActivity = CaptureActivityPortrait::class.java
|
scanOptions.setOrientationLocked(true)
|
||||||
scanLauncher.launch(scanOptions)
|
scanOptions.setPrompt(context.getString(R.string.scanning_qr))
|
||||||
}
|
scanOptions.setBeepEnabled(false)
|
||||||
}
|
scanOptions.captureActivity = CaptureActivityPortrait::class.java
|
||||||
.padding(10.dp)) {
|
scanLauncher.launch(scanOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(10.dp)
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.QrCode,
|
Icons.Filled.QrCode,
|
||||||
contentDescription = stringResource(id = R.string.qr_scan),
|
contentDescription = stringResource(id = R.string.qr_scan),
|
||||||
modifier = Modifier.padding(10.dp))
|
modifier = Modifier.padding(10.dp)
|
||||||
|
)
|
||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.add_from_qr),
|
stringResource(id = R.string.add_from_qr),
|
||||||
modifier = Modifier.padding(10.dp))
|
modifier = Modifier.padding(10.dp)
|
||||||
}
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Divider()
|
Divider()
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier = Modifier
|
||||||
Modifier.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable {
|
.clickable {
|
||||||
showBottomSheet = false
|
showBottomSheet = false
|
||||||
navController.navigate(
|
navController.navigate("${Routes.Config.name}/${Constants.MANUAL_TUNNEL_CONFIG_ID}")
|
||||||
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}")
|
}
|
||||||
}
|
.padding(10.dp)
|
||||||
.padding(10.dp)) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.Create,
|
Icons.Filled.Create,
|
||||||
contentDescription = stringResource(id = R.string.create_import),
|
contentDescription = stringResource(id = R.string.create_import),
|
||||||
modifier = Modifier.padding(10.dp))
|
modifier = Modifier.padding(10.dp)
|
||||||
Text(
|
)
|
||||||
stringResource(id = R.string.create_import),
|
Text(
|
||||||
modifier = Modifier.padding(10.dp))
|
stringResource(id = R.string.create_import),
|
||||||
}
|
modifier = Modifier.padding(10.dp)
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Column(
|
||||||
LazyColumn(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier =
|
modifier = Modifier
|
||||||
Modifier.fillMaxWidth().fillMaxHeight(.90f).overscroll(ScrollableDefaults.overscrollEffect()).padding(innerPadding),
|
.fillMaxSize()
|
||||||
state = rememberLazyListState(0, uiState.tunnels.count()),
|
.padding(padding)
|
||||||
userScrollEnabled = true,
|
) {
|
||||||
reverseLayout = true,
|
LazyColumn(
|
||||||
flingBehavior = ScrollableDefaults.flingBehavior()) {
|
modifier = Modifier
|
||||||
items(uiState.tunnels,
|
.fillMaxSize()
|
||||||
key = { tunnel -> tunnel.id }) { tunnel ->
|
.nestedScroll(nestedScrollConnection),
|
||||||
val leadingIconColor =
|
) {
|
||||||
(if (uiState.vpnState.name == tunnel.name &&
|
items(tunnels, key = { tunnel -> tunnel.id }) { tunnel ->
|
||||||
uiState.vpnState.status == Tunnel.State.UP) {
|
val leadingIconColor = (if (tunnelName == tunnel.name) when (handshakeStatus) {
|
||||||
uiState.vpnState.statistics
|
HandshakeStatus.HEALTHY -> mint
|
||||||
?.mapPeerStats()
|
HandshakeStatus.UNHEALTHY -> brickRed
|
||||||
?.map { it.value?.handshakeStatus() }
|
HandshakeStatus.NOT_STARTED -> Color.Gray
|
||||||
.let { statuses ->
|
HandshakeStatus.NEVER_CONNECTED -> brickRed
|
||||||
when {
|
} else {Color.Gray})
|
||||||
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> mint
|
val focusRequester = remember { FocusRequester() }
|
||||||
statuses?.any { it == HandshakeStatus.STALE } == true -> corn
|
val expanded = remember {
|
||||||
statuses?.all { it == HandshakeStatus.NOT_STARTED } == true ->
|
mutableStateOf(false)
|
||||||
Color.Gray
|
}
|
||||||
else -> {
|
RowListItem(icon = {
|
||||||
Color.Gray
|
if (settings.isTunnelConfigDefault(tunnel))
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Color.Gray
|
|
||||||
})
|
|
||||||
val expanded = remember { mutableStateOf(false) }
|
|
||||||
RowListItem(
|
|
||||||
icon = {
|
|
||||||
if (uiState.settings.isTunnelConfigDefault(tunnel)) {
|
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Star,
|
Icons.Rounded.Star, stringResource(R.string.status),
|
||||||
stringResource(R.string.status),
|
|
||||||
tint = leadingIconColor,
|
tint = leadingIconColor,
|
||||||
modifier = Modifier.padding(end = 10.dp).size(20.dp))
|
modifier = Modifier
|
||||||
} else {
|
.padding(end = 10.dp)
|
||||||
Icon(
|
.size(20.dp)
|
||||||
Icons.Rounded.Circle,
|
)
|
||||||
stringResource(R.string.status),
|
else Icon(
|
||||||
tint = leadingIconColor,
|
Icons.Rounded.Circle, stringResource(R.string.status),
|
||||||
modifier = Modifier.padding(end = 15.dp).size(15.dp))
|
tint = leadingIconColor,
|
||||||
}
|
modifier = Modifier
|
||||||
},
|
.padding(end = 15.dp)
|
||||||
|
.size(15.dp)
|
||||||
|
)
|
||||||
|
},
|
||||||
text = tunnel.name,
|
text = tunnel.name,
|
||||||
onHold = {
|
onHold = {
|
||||||
if ((uiState.vpnState.status == Tunnel.State.UP) &&
|
if (state == Tunnel.State.UP && tunnel.name == tunnelName) {
|
||||||
(tunnel.name == uiState.vpnState.name)) {
|
showSnackbarMessage(context.resources.getString(R.string.turn_off_tunnel))
|
||||||
showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
return@RowListItem
|
||||||
return@RowListItem
|
}
|
||||||
}
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
selectedTunnel = tunnel
|
||||||
selectedTunnel = tunnel
|
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
if (uiState.vpnState.status == Tunnel.State.UP &&
|
if(state == Tunnel.State.UP && (tunnelName == tunnel.name) ) {
|
||||||
(uiState.vpnState.name == tunnel.name)) {
|
expanded.value = !expanded.value
|
||||||
expanded.value = !expanded.value
|
}
|
||||||
|
} else {
|
||||||
|
selectedTunnel = tunnel
|
||||||
|
focusRequester.requestFocus()
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
selectedTunnel = tunnel
|
|
||||||
focusRequester.requestFocus()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
statistics = uiState.vpnState.statistics,
|
statistics = statistics,
|
||||||
expanded = expanded.value,
|
expanded = expanded.value,
|
||||||
rowButton = {
|
rowButton = {
|
||||||
if (tunnel.id == selectedTunnel?.id &&
|
if (tunnel.id == selectedTunnel?.id && !WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
Row {
|
||||||
Row {
|
if(!settings.isTunnelConfigDefault(tunnel)) {
|
||||||
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
|
IconButton(onClick = {
|
||||||
IconButton(
|
if(settings.isAutoTunnelEnabled) {
|
||||||
onClick = {
|
showSnackbarMessage(context.resources.getString(R.string.turn_off_auto))
|
||||||
if (uiState.settings.isAutoTunnelEnabled && !uiState.settings.isAutoTunnelPaused) {
|
} else showPrimaryChangeAlertDialog = true
|
||||||
showSnackbarMessage(
|
}) {
|
||||||
Event.Message.AutoTunnelOffAction.message)
|
Icon(Icons.Rounded.Star, stringResource(id = R.string.set_primary))
|
||||||
} else {
|
}
|
||||||
showPrimaryChangeAlertDialog = true
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
Icons.Rounded.Star,
|
|
||||||
stringResource(id = R.string.set_primary))
|
|
||||||
}
|
}
|
||||||
}
|
IconButton(onClick = {
|
||||||
IconButton(
|
navController.navigate("${Routes.Config.name}/${selectedTunnel?.id}")
|
||||||
onClick = {
|
}) {
|
||||||
if (uiState.settings.isAutoTunnelEnabled && uiState.settings.isTunnelConfigDefault(tunnel)
|
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
|
||||||
&& !uiState.settings.isAutoTunnelPaused) {
|
}
|
||||||
showSnackbarMessage(
|
IconButton(
|
||||||
Event.Message.AutoTunnelOffAction.message)
|
modifier = Modifier.focusable(),
|
||||||
} else navController.navigate(
|
onClick = { viewModel.onDelete(tunnel) }) {
|
||||||
"${Screen.Config.route}/${selectedTunnel?.id}")
|
Icon(
|
||||||
}) {
|
Icons.Rounded.Delete,
|
||||||
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
|
stringResource(id = R.string.delete)
|
||||||
}
|
)
|
||||||
IconButton(
|
}
|
||||||
modifier = Modifier.focusable(),
|
}
|
||||||
onClick = { viewModel.onDelete(tunnel) }) {
|
} else {
|
||||||
Icon(Icons.Rounded.Delete, stringResource(id = R.string.delete))
|
val checked = state == Tunnel.State.UP && tunnel.name == tunnelName
|
||||||
}
|
if(!checked) expanded.value = false
|
||||||
}
|
@Composable
|
||||||
} else {
|
fun TunnelSwitch() = Switch(
|
||||||
val checked by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
(uiState.vpnState.status == Tunnel.State.UP &&
|
|
||||||
tunnel.name == uiState.vpnState.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!checked) expanded.value = false
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun TunnelSwitch() =
|
|
||||||
Switch(
|
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
checked = checked,
|
checked = checked,
|
||||||
onCheckedChange = { checked ->
|
onCheckedChange = { checked ->
|
||||||
if (!checked) expanded.value = false
|
if(!checked) expanded.value = false
|
||||||
onTunnelToggle(checked, tunnel)
|
onTunnelToggle(checked, tunnel)
|
||||||
})
|
}
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
)
|
||||||
Row {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
|
Row {
|
||||||
IconButton(
|
if(!settings.isTunnelConfigDefault(tunnel)) {
|
||||||
onClick = {
|
IconButton(onClick = {
|
||||||
if (uiState.settings.isAutoTunnelEnabled) {
|
if(settings.isAutoTunnelEnabled) {
|
||||||
showSnackbarMessage(
|
showSnackbarMessage(context.resources.getString(R.string.turn_off_auto))
|
||||||
Event.Message.AutoTunnelOffAction.message)
|
} else showPrimaryChangeAlertDialog = true
|
||||||
} else {
|
}) {
|
||||||
selectedTunnel = tunnel
|
Icon(Icons.Rounded.Star, stringResource(id = R.string.set_primary))
|
||||||
showPrimaryChangeAlertDialog = true
|
}
|
||||||
}
|
}
|
||||||
}) {
|
IconButton(
|
||||||
Icon(
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
Icons.Rounded.Star,
|
onClick = {
|
||||||
stringResource(id = R.string.set_primary))
|
if(state == Tunnel.State.UP && (tunnelName == tunnel.name) ) {
|
||||||
}
|
expanded.value = !expanded.value
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(Icons.Rounded.Info, stringResource(R.string.info))
|
||||||
|
}
|
||||||
|
IconButton(onClick = {
|
||||||
|
if (state == Tunnel.State.UP && tunnel.name == tunnelName)
|
||||||
|
showSnackbarMessage(
|
||||||
|
context.resources.getString(
|
||||||
|
R.string.turn_off_tunnel
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else {
|
||||||
|
navController.navigate("${Routes.Config.name}/${tunnel.id}")
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Edit,
|
||||||
|
stringResource(id = R.string.edit)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(onClick = {
|
||||||
|
if (state == Tunnel.State.UP && tunnel.name == tunnelName)
|
||||||
|
showSnackbarMessage(
|
||||||
|
context.resources.getString(
|
||||||
|
R.string.turn_off_tunnel
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else {
|
||||||
|
viewModel.onDelete(tunnel)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Delete,
|
||||||
|
stringResource(id = R.string.delete)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TunnelSwitch()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TunnelSwitch()
|
||||||
}
|
}
|
||||||
IconButton(
|
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
|
||||||
onClick = {
|
|
||||||
if (uiState.vpnState.status == Tunnel.State.UP &&
|
|
||||||
(uiState.vpnState.name == tunnel.name)) {
|
|
||||||
expanded.value = !expanded.value
|
|
||||||
} else {
|
|
||||||
showSnackbarMessage(Event.Message.TunnelOnAction.message)
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Icon(Icons.Rounded.Info, stringResource(R.string.info))
|
|
||||||
}
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
if (uiState.vpnState.status == Tunnel.State.UP &&
|
|
||||||
tunnel.name == uiState.vpnState.name) {
|
|
||||||
showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
|
||||||
} else {
|
|
||||||
navController.navigate(
|
|
||||||
"${Screen.Config.route}/${tunnel.id}")
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
|
|
||||||
}
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
if (uiState.vpnState.status == Tunnel.State.UP &&
|
|
||||||
tunnel.name == uiState.vpnState.name) {
|
|
||||||
showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
|
||||||
} else {
|
|
||||||
viewModel.onDelete(tunnel)
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
Icons.Rounded.Delete,
|
|
||||||
stringResource(id = R.string.delete))
|
|
||||||
}
|
|
||||||
TunnelSwitch()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
TunnelSwitch()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
|
||||||
|
|
||||||
data class MainUiState(
|
|
||||||
val settings : Settings = Settings(),
|
|
||||||
val tunnels : TunnelConfigs = emptyList(),
|
|
||||||
val vpnState: VpnState = VpnState(),
|
|
||||||
val loading : Boolean = true
|
|
||||||
)
|
|
||||||
+204
-206
@@ -8,254 +8,252 @@ import android.provider.OpenableColumns
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceState
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceState
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
|
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class MainViewModel
|
class MainViewModel @Inject constructor(
|
||||||
@Inject
|
|
||||||
constructor(
|
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
private val tunnelConfigRepository: TunnelConfigRepository,
|
private val tunnelRepo: TunnelConfigDao,
|
||||||
private val settingsRepository: SettingsRepository,
|
private val settingsRepo: SettingsDoa,
|
||||||
private val vpnService: VpnService
|
private val vpnService: VpnService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val uiState =
|
val tunnels get() = tunnelRepo.getAllFlow()
|
||||||
combine(
|
val state get() = vpnService.state
|
||||||
settingsRepository.getSettingsFlow(),
|
|
||||||
tunnelConfigRepository.getTunnelConfigsFlow(),
|
|
||||||
vpnService.vpnState,
|
|
||||||
) { settings, tunnels, vpnState ->
|
|
||||||
validateWatcherServiceState(settings)
|
|
||||||
MainUiState(settings, tunnels, vpnState, false)
|
|
||||||
}
|
|
||||||
.stateIn(
|
|
||||||
viewModelScope,
|
|
||||||
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
|
||||||
MainUiState())
|
|
||||||
|
|
||||||
private fun validateWatcherServiceState(settings: Settings) = viewModelScope.launch(Dispatchers.IO) {
|
val handshakeStatus get() = vpnService.handshakeStatus
|
||||||
val watcherState =
|
val tunnelName get() = vpnService.tunnelName
|
||||||
ServiceManager.getServiceState(
|
private val _settings = MutableStateFlow(Settings())
|
||||||
application.applicationContext, WireGuardConnectivityWatcherService::class.java)
|
val settings get() = _settings.asStateFlow()
|
||||||
if (settings.isAutoTunnelEnabled &&
|
val statistics get() = vpnService.statistics
|
||||||
watcherState == ServiceState.STOPPED) {
|
|
||||||
ServiceManager.startWatcherService(application.applicationContext)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopWatcherService() = viewModelScope.launch(Dispatchers.IO) {
|
init {
|
||||||
ServiceManager.stopWatcherService(application.applicationContext)
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
}
|
settingsRepo.getAllFlow().filter { it.isNotEmpty() }.collect {
|
||||||
fun onDelete(tunnel: TunnelConfig) {
|
val settings = it.first()
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
validateWatcherServiceState(settings)
|
||||||
if (tunnelConfigRepository.count() == 1) {
|
_settings.emit(settings)
|
||||||
stopWatcherService()
|
}
|
||||||
val settings = settingsRepository.getSettings()
|
|
||||||
settings.defaultTunnel = null
|
|
||||||
settings.isAutoTunnelEnabled = false
|
|
||||||
settings.isAlwaysOnVpnEnabled = false
|
|
||||||
saveSettings(settings)
|
|
||||||
}
|
|
||||||
tunnelConfigRepository.delete(tunnel)
|
|
||||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onTunnelStart(tunnelConfig: TunnelConfig) = viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
stopActiveTunnel().await()
|
|
||||||
startTunnel(tunnelConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopActiveTunnel() =
|
|
||||||
viewModelScope.async(Dispatchers.IO) {
|
|
||||||
if (ServiceManager.getServiceState(
|
|
||||||
application.applicationContext, WireGuardTunnelService::class.java) ==
|
|
||||||
ServiceState.STARTED) {
|
|
||||||
onTunnelStop()
|
|
||||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTunnelStop() = viewModelScope.launch(Dispatchers.IO) {
|
private fun validateWatcherServiceState(settings: Settings) {
|
||||||
ServiceManager.stopVpnService(application.applicationContext)
|
val watcherState = ServiceManager.getServiceState(
|
||||||
}
|
application.applicationContext,
|
||||||
|
WireGuardConnectivityWatcherService::class.java
|
||||||
private fun validateConfigString(config: String) {
|
)
|
||||||
TunnelConfig.configFromQuick(config)
|
if (settings.isAutoTunnelEnabled && watcherState == ServiceState.STOPPED && settings.defaultTunnel != null) {
|
||||||
}
|
ServiceManager.startWatcherService(
|
||||||
|
application.applicationContext,
|
||||||
suspend fun onTunnelQrResult(result: String) : Result<Unit> {
|
settings.defaultTunnel!!
|
||||||
return try {
|
)
|
||||||
validateConfigString(result)
|
|
||||||
val tunnelConfig =
|
|
||||||
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
|
|
||||||
addTunnel(tunnelConfig)
|
|
||||||
Result.Success(Unit)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Result.Error(Event.Error.InvalidQrCode)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) {
|
|
||||||
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
|
|
||||||
val config = Config.parse(bufferReader)
|
|
||||||
val tunnelName = getNameFromFileName(fileName)
|
|
||||||
addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
|
|
||||||
withContext(Dispatchers.IO) { stream.close() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getInputStreamFromUri(uri: Uri): InputStream? {
|
fun onDelete(tunnel: TunnelConfig) {
|
||||||
return application.applicationContext.contentResolver.openInputStream(uri)
|
viewModelScope.launch {
|
||||||
}
|
if (tunnelRepo.count() == 1L) {
|
||||||
|
ServiceManager.stopWatcherService(application.applicationContext)
|
||||||
suspend fun onTunnelFileSelected(uri: Uri) : Result<Unit> {
|
val settings = settingsRepo.getAll()
|
||||||
try {
|
if (settings.isNotEmpty()) {
|
||||||
if(isValidUriContentScheme(uri)){
|
val setting = settings[0]
|
||||||
val fileName = getFileName(application.applicationContext, uri)
|
setting.defaultTunnel = null
|
||||||
when (getFileExtensionFromFileName(fileName)) {
|
setting.isAutoTunnelEnabled = false
|
||||||
Constants.CONF_FILE_EXTENSION -> saveTunnelFromConfUri(fileName, uri).let {
|
setting.isAlwaysOnVpnEnabled = false
|
||||||
when(it) {
|
settingsRepo.save(setting)
|
||||||
is Result.Error -> return Result.Error(Event.Error.FileReadFailed)
|
|
||||||
is Result.Success -> return it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
|
|
||||||
else -> return Result.Error(Event.Error.InvalidFileExtension)
|
|
||||||
}
|
}
|
||||||
return Result.Success(Unit)
|
}
|
||||||
} else {
|
tunnelRepo.delete(tunnel)
|
||||||
return Result.Error(Event.Error.InvalidFileExtension)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onTunnelStart(tunnelConfig: TunnelConfig) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
stopActiveTunnel()
|
||||||
|
startTunnel(tunnelConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startTunnel(tunnelConfig: TunnelConfig) {
|
||||||
|
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun stopActiveTunnel() {
|
||||||
|
if (ServiceManager.getServiceState(
|
||||||
|
application.applicationContext,
|
||||||
|
WireGuardTunnelService::class.java,
|
||||||
|
) == ServiceState.STARTED
|
||||||
|
) {
|
||||||
|
onTunnelStop()
|
||||||
|
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onTunnelStop() {
|
||||||
|
ServiceManager.stopVpnService(application.applicationContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateConfigString(config: String) {
|
||||||
|
TunnelConfig.configFromQuick(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun onTunnelQrResult(result: String) {
|
||||||
|
try {
|
||||||
|
validateConfigString(result)
|
||||||
|
val tunnelConfig =
|
||||||
|
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
|
||||||
|
addTunnel(tunnelConfig)
|
||||||
|
} catch (e : Exception) {
|
||||||
|
throw WgTunnelException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) {
|
||||||
|
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
|
||||||
|
val config = Config.parse(bufferReader)
|
||||||
|
val tunnelName = getNameFromFileName(fileName)
|
||||||
|
addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
stream.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getInputStreamFromUri(uri: Uri): InputStream {
|
||||||
|
return application.applicationContext.contentResolver.openInputStream(uri)
|
||||||
|
?: throw WgTunnelException(application.getString(R.string.stream_failed))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun onTunnelFileSelected(uri: Uri) {
|
||||||
|
try {
|
||||||
|
val fileName = getFileName(application.applicationContext, uri)
|
||||||
|
val fileExtension = getFileExtensionFromFileName(fileName)
|
||||||
|
when(fileExtension){
|
||||||
|
Constants.CONF_FILE_EXTENSION -> saveTunnelFromConfUri(fileName, uri)
|
||||||
|
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
|
||||||
|
else -> throw WgTunnelException(application.getString(R.string.file_extension_message))
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return Result.Error(Event.Error.FileReadFailed)
|
throw WgTunnelException(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveTunnelsFromZipUri(uri: Uri) {
|
private suspend fun saveTunnelsFromZipUri(uri: Uri) {
|
||||||
ZipInputStream(getInputStreamFromUri(uri)).use { zip ->
|
ZipInputStream(getInputStreamFromUri(uri)).use { zip ->
|
||||||
generateSequence { zip.nextEntry }
|
generateSequence { zip.nextEntry }
|
||||||
.filterNot {
|
.filterNot { it.isDirectory ||
|
||||||
it.isDirectory || getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
|
getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION }
|
||||||
}
|
.forEach {
|
||||||
.forEach {
|
val name = getNameFromFileName(it.name)
|
||||||
val name = getNameFromFileName(it.name)
|
val config = Config.parse(zip)
|
||||||
val config = Config.parse(zip)
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
addTunnel(TunnelConfig(name = name, wgQuick = config.toWgQuickString()))
|
||||||
addTunnel(TunnelConfig(name = name, wgQuick = config.toWgQuickString()))
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun saveTunnelFromConfUri(name: String, uri: Uri) : Result<Unit> {
|
|
||||||
val stream = getInputStreamFromUri(uri)
|
|
||||||
return if(stream != null) {
|
|
||||||
saveTunnelConfigFromStream(stream, name)
|
|
||||||
Result.Success(Unit)
|
|
||||||
} else {
|
|
||||||
Result.Error(Event.Error.FileReadFailed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
|
||||||
saveTunnel(tunnelConfig)
|
|
||||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun pauseAutoTunneling() = viewModelScope.launch {
|
|
||||||
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resumeAutoTunneling() = viewModelScope.launch {
|
private suspend fun saveTunnelFromConfUri(name : String, uri: Uri) {
|
||||||
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
|
val stream = getInputStreamFromUri(uri)
|
||||||
|
saveTunnelConfigFromStream(stream, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
|
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
||||||
tunnelConfigRepository.save(tunnelConfig)
|
saveTunnel(tunnelConfig)
|
||||||
}
|
|
||||||
|
|
||||||
private fun getFileNameByCursor(context: Context, uri: Uri): String? {
|
|
||||||
context.contentResolver.query(uri, null, null, null, null)?.use {
|
|
||||||
return getDisplayNameByCursor(it)
|
|
||||||
}
|
}
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getDisplayNameColumnIndex(cursor: Cursor): Int? {
|
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
|
||||||
val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
tunnelRepo.save(tunnelConfig)
|
||||||
return if (columnIndex != -1) {
|
|
||||||
return columnIndex
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun getDisplayNameByCursor(cursor: Cursor): String? {
|
private fun getFileNameByCursor(context: Context, uri: Uri): String {
|
||||||
return if (cursor.moveToFirst()) {
|
val cursor = context.contentResolver.query(uri, null, null, null, null)
|
||||||
val index = getDisplayNameColumnIndex(cursor)
|
if (cursor != null) {
|
||||||
if (index != null) {
|
cursor.use {
|
||||||
cursor.getString(index)
|
return getDisplayNameByCursor(it)
|
||||||
} else null
|
}
|
||||||
} else null
|
} else {
|
||||||
}
|
throw WgTunnelException("Failed to initialize cursor")
|
||||||
|
}
|
||||||
private fun isValidUriContentScheme(uri: Uri): Boolean {
|
|
||||||
return uri.scheme == Constants.URI_CONTENT_SCHEME
|
|
||||||
}
|
|
||||||
private fun getFileName(context: Context, uri: Uri): String {
|
|
||||||
return getFileNameByCursor(context, uri) ?: NumberUtils.generateRandomTunnelName()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getNameFromFileName(fileName: String): String {
|
|
||||||
return fileName.substring(0, fileName.lastIndexOf('.'))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getFileExtensionFromFileName(fileName: String): String {
|
|
||||||
return try {
|
|
||||||
fileName.substring(fileName.lastIndexOf('.'))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
""
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveSettings(settings: Settings) =
|
private fun getDisplayNameColumnIndex(cursor: Cursor): Int {
|
||||||
viewModelScope.launch(Dispatchers.IO) { settingsRepository.save(settings) }
|
val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||||
|
if (columnIndex == -1) {
|
||||||
fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) = viewModelScope.launch {
|
throw WgTunnelException("Cursor out of bounds")
|
||||||
if (selectedTunnel != null) {
|
}
|
||||||
saveSettings(uiState.value.settings.copy(defaultTunnel = selectedTunnel.toString())).join()
|
return columnIndex
|
||||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
}
|
||||||
|
|
||||||
|
private fun getDisplayNameByCursor(cursor: Cursor): String {
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
val index = getDisplayNameColumnIndex(cursor)
|
||||||
|
return cursor.getString(index)
|
||||||
|
} else {
|
||||||
|
throw WgTunnelException("Cursor failed to move to first")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateUriContentScheme(uri: Uri) {
|
||||||
|
if (uri.scheme != Constants.URI_CONTENT_SCHEME) {
|
||||||
|
throw WgTunnelException(application.getString(R.string.file_extension_message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun getFileName(context: Context, uri: Uri): String {
|
||||||
|
validateUriContentScheme(uri)
|
||||||
|
return try {
|
||||||
|
getFileNameByCursor(context, uri)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
NumberUtils.generateRandomTunnelName()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getNameFromFileName(fileName: String): String {
|
||||||
|
return fileName.substring(0, fileName.lastIndexOf('.'))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFileExtensionFromFileName(fileName: String): String {
|
||||||
|
return try {
|
||||||
|
fileName.substring(fileName.lastIndexOf('.'))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) {
|
||||||
|
if (selectedTunnel != null) {
|
||||||
|
_settings.emit(
|
||||||
|
_settings.value.copy(
|
||||||
|
defaultTunnel = selectedTunnel.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
settingsRepo.save(_settings.value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+382
-402
@@ -4,7 +4,7 @@ import android.Manifest
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
|
import android.provider.Settings
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
@@ -30,7 +30,6 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.outlined.Add
|
import androidx.compose.material.icons.outlined.Add
|
||||||
import androidx.compose.material.icons.rounded.LocationOff
|
import androidx.compose.material.icons.rounded.LocationOff
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -39,20 +38,21 @@ import androidx.compose.material3.Surface
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.SideEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusProperties
|
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
@@ -65,444 +65,424 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.isGranted
|
import com.google.accompanist.permissions.isGranted
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
import com.wireguard.android.backend.Tunnel
|
|
||||||
import com.wireguard.android.backend.WgQuickBackend
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
import com.zaneschepke.wireguardautotunnel.util.StorageUtil
|
||||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@OptIn(
|
@OptIn(
|
||||||
ExperimentalPermissionsApi::class,
|
ExperimentalPermissionsApi::class,
|
||||||
ExperimentalLayoutApi::class)
|
ExperimentalLayoutApi::class, ExperimentalComposeUiApi::class
|
||||||
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
viewModel: SettingsViewModel = hiltViewModel(),
|
viewModel: SettingsViewModel = hiltViewModel(),
|
||||||
padding: PaddingValues,
|
padding: PaddingValues,
|
||||||
showSnackbarMessage: (String) -> Unit,
|
showSnackbarMessage: (String) -> Unit,
|
||||||
focusRequester: FocusRequester
|
focusRequester: FocusRequester,
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
|
||||||
val context = LocalContext.current
|
|
||||||
val focusManager = LocalFocusManager.current
|
|
||||||
val scrollState = rememberScrollState()
|
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
|
||||||
|
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||||
|
val context = LocalContext.current
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
||||||
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
val settings by viewModel.settings.collectAsStateWithLifecycle()
|
||||||
var currentText by remember { mutableStateOf("") }
|
val trustedSSIDs by viewModel.trustedSSIDs.collectAsStateWithLifecycle()
|
||||||
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
|
val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf())
|
||||||
var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
|
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
var didExportFiles by remember { mutableStateOf(false) }
|
var currentText by remember { mutableStateOf("") }
|
||||||
var showAuthPrompt by remember { mutableStateOf(false) }
|
val scrollState = rememberScrollState()
|
||||||
val focusRequester2 = remember { FocusRequester() }
|
var isLocationDisclaimerNeeded by remember { mutableStateOf(true) }
|
||||||
|
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
|
||||||
|
var showAuthPrompt by remember { mutableStateOf(false) }
|
||||||
|
var didExportFiles by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val screenPadding = 5.dp
|
|
||||||
val fillMaxWidth = .85f
|
|
||||||
|
|
||||||
if (uiState.loading) {
|
|
||||||
LoadingScreen()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fun exportAllConfigs() {
|
val screenPadding = 5.dp
|
||||||
try {
|
val fillMaxWidth = .85f
|
||||||
val files = uiState.tunnels.map { File(context.cacheDir, "${it.name}.conf") }
|
|
||||||
files.forEachIndexed { index, file ->
|
fun exportAllConfigs() {
|
||||||
file.outputStream().use { it.write(uiState.tunnels[index].wgQuick.toByteArray()) }
|
try {
|
||||||
}
|
val files = tunnels.map { File(context.cacheDir, "${it.name}.conf") }
|
||||||
FileUtils.saveFilesToZip(context, files)
|
files.forEachIndexed { index, file ->
|
||||||
didExportFiles = true
|
file.outputStream().use {
|
||||||
showSnackbarMessage(Event.Message.ConfigsExported.message)
|
it.write(tunnels[index].wgQuick.toByteArray())
|
||||||
} catch (e: Exception) {
|
}
|
||||||
showSnackbarMessage(Event.Error.Exception(e).message)
|
}
|
||||||
|
StorageUtil.saveFilesToZip(context, files)
|
||||||
|
didExportFiles = true
|
||||||
|
showSnackbarMessage(context.getString(R.string.exported_configs_message))
|
||||||
|
} catch (e : Exception) {
|
||||||
|
showSnackbarMessage(e.message!!)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun saveTrustedSSID() {
|
|
||||||
if (currentText.isNotEmpty()) {
|
fun saveTrustedSSID() {
|
||||||
viewModel.onSaveTrustedSSID(currentText).let {
|
if (currentText.isNotEmpty()) {
|
||||||
when(it) {
|
scope.launch {
|
||||||
is Result.Success -> currentText = ""
|
try {
|
||||||
is Result.Error -> showSnackbarMessage(it.error.message)
|
viewModel.onSaveTrustedSSID(currentText)
|
||||||
}
|
currentText = ""
|
||||||
}
|
} catch (e : Exception) {
|
||||||
|
showSnackbarMessage(e.message ?: context.getString(R.string.unknown_error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun openSettings() {
|
fun isAllAutoTunnelPermissionsEnabled() : Boolean {
|
||||||
scope.launch {
|
return(isBackgroundLocationGranted && fineLocationState.status.isGranted && !viewModel.isLocationServicesNeeded())
|
||||||
val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
|
|
||||||
intentSettings.data = Uri.fromParts("package", context.packageName, null)
|
|
||||||
context.startActivity(intentSettings)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun checkFineLocationGranted() {
|
|
||||||
isBackgroundLocationGranted =
|
fun openSettings() {
|
||||||
if (!fineLocationState.status.isGranted) {
|
scope.launch {
|
||||||
false
|
val intentSettings =
|
||||||
|
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||||
|
intentSettings.data =
|
||||||
|
Uri.fromParts("package", context.packageName, null)
|
||||||
|
context.startActivity(intentSettings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
val backgroundLocationState =
|
||||||
|
rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
||||||
|
if(!backgroundLocationState.status.isGranted) {
|
||||||
|
isBackgroundLocationGranted = false
|
||||||
} else {
|
} else {
|
||||||
viewModel.setLocationDisclosureShown()
|
isLocationDisclaimerNeeded = false
|
||||||
true
|
isBackgroundLocationGranted = true
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
if(WireGuardAutoTunnel.isRunningOnAndroidTv() && Build.VERSION.SDK_INT == Build.VERSION_CODES.Q){
|
|
||||||
checkFineLocationGranted()
|
|
||||||
} else {
|
|
||||||
val backgroundLocationState =
|
|
||||||
rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
|
||||||
isBackgroundLocationGranted =
|
|
||||||
if (!backgroundLocationState.status.isGranted) {
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
SideEffect { viewModel.setLocationDisclosureShown() }
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
|
|
||||||
checkFineLocationGranted()
|
|
||||||
}
|
|
||||||
|
|
||||||
AnimatedVisibility(showLocationServicesAlertDialog) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { showLocationServicesAlertDialog = false },
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
showLocationServicesAlertDialog = false
|
|
||||||
viewModel.toggleAutoTunnel()
|
|
||||||
}) {
|
|
||||||
Text(text = stringResource(R.string.okay))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = { showLocationServicesAlertDialog = false }) {
|
|
||||||
Text(text = stringResource(R.string.cancel))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title = { Text(text = stringResource(R.string.location_services_not_detected)) },
|
|
||||||
text = { Text(text = stringResource(R.string.location_services_missing_message)) })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!uiState.isLocationDisclosureShown) {
|
if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
|
||||||
Column(
|
if(!fineLocationState.status.isGranted) {
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
isBackgroundLocationGranted = false
|
||||||
verticalArrangement = Arrangement.Top,
|
} else {
|
||||||
modifier = Modifier.fillMaxSize().verticalScroll(scrollState).padding(padding)) {
|
isLocationDisclaimerNeeded = false
|
||||||
Icon(
|
isBackgroundLocationGranted = true
|
||||||
Icons.Rounded.LocationOff,
|
|
||||||
contentDescription = stringResource(id = R.string.map),
|
|
||||||
modifier = Modifier.padding(30.dp).size(128.dp))
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.prominent_background_location_title),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.padding(30.dp),
|
|
||||||
fontSize = 20.sp)
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.prominent_background_location_message),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.padding(30.dp),
|
|
||||||
fontSize = 15.sp)
|
|
||||||
Row(
|
|
||||||
modifier =
|
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
|
||||||
Modifier.fillMaxWidth().padding(10.dp)
|
|
||||||
} else {
|
|
||||||
Modifier.fillMaxWidth().padding(30.dp)
|
|
||||||
},
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly) {
|
|
||||||
TextButton(onClick = { viewModel.setLocationDisclosureShown() }) {
|
|
||||||
Text(stringResource(id = R.string.no_thanks))
|
|
||||||
}
|
|
||||||
TextButton(
|
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
|
||||||
onClick = {
|
|
||||||
openSettings()
|
|
||||||
viewModel.setLocationDisclosureShown()
|
|
||||||
}) {
|
|
||||||
Text(stringResource(id = R.string.turn_on))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(showAuthPrompt) {
|
if(isLocationDisclaimerNeeded) {
|
||||||
AuthorizationPrompt(
|
Column(
|
||||||
onSuccess = {
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
showAuthPrompt = false
|
verticalArrangement = Arrangement.Top,
|
||||||
exportAllConfigs()
|
modifier = Modifier
|
||||||
},
|
.fillMaxSize()
|
||||||
onError = { _ ->
|
.verticalScroll(scrollState)
|
||||||
showAuthPrompt = false
|
.padding(padding)
|
||||||
showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
) {
|
||||||
},
|
Icon(
|
||||||
onFailure = {
|
Icons.Rounded.LocationOff,
|
||||||
showAuthPrompt = false
|
contentDescription = stringResource(id = R.string.map),
|
||||||
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
modifier = Modifier
|
||||||
})
|
.padding(30.dp)
|
||||||
}
|
.size(128.dp)
|
||||||
|
)
|
||||||
if (uiState.tunnels.isEmpty() && uiState.isLocationDisclosureShown) {
|
Text(
|
||||||
Column(
|
stringResource(R.string.prominent_background_location_title),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
textAlign = TextAlign.Center,
|
||||||
verticalArrangement = Arrangement.Center,
|
modifier = Modifier.padding(30.dp),
|
||||||
modifier = Modifier.fillMaxSize().padding(padding)) {
|
fontSize = 20.sp
|
||||||
Text(
|
)
|
||||||
stringResource(R.string.one_tunnel_required),
|
Text(
|
||||||
textAlign = TextAlign.Center,
|
stringResource(R.string.prominent_background_location_message),
|
||||||
modifier = Modifier.padding(15.dp),
|
textAlign = TextAlign.Center,
|
||||||
fontStyle = FontStyle.Italic)
|
modifier = Modifier.padding(30.dp),
|
||||||
}
|
fontSize = 15.sp
|
||||||
}
|
)
|
||||||
if (uiState.isLocationDisclosureShown && uiState.tunnels.isNotEmpty()) {
|
Row(
|
||||||
Column(
|
modifier = if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) Modifier
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
.fillMaxWidth()
|
||||||
verticalArrangement = Arrangement.Top,
|
.padding(10.dp) else Modifier
|
||||||
modifier =
|
.fillMaxWidth()
|
||||||
Modifier.fillMaxSize().verticalScroll(scrollState).clickable(
|
.padding(30.dp),
|
||||||
indication = null, interactionSource = interactionSource) {
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
focusManager.clearFocus()
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
|
) {
|
||||||
|
TextButton(onClick = {
|
||||||
|
isLocationDisclaimerNeeded = false
|
||||||
}) {
|
}) {
|
||||||
Surface(
|
Text(stringResource(id = R.string.no_thanks))
|
||||||
tonalElevation = 2.dp,
|
}
|
||||||
shadowElevation = 2.dp,
|
TextButton(modifier = Modifier.focusRequester(focusRequester), onClick = {
|
||||||
shape = RoundedCornerShape(12.dp),
|
openSettings()
|
||||||
color = MaterialTheme.colorScheme.surface,
|
}) {
|
||||||
modifier =
|
Text(stringResource(id = R.string.turn_on))
|
||||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
}
|
||||||
Modifier.height(IntrinsicSize.Min)
|
}
|
||||||
.fillMaxWidth(fillMaxWidth)
|
}
|
||||||
.padding(top = 10.dp)
|
return
|
||||||
} else {
|
}
|
||||||
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 60.dp)
|
|
||||||
})
|
|
||||||
.padding(bottom = 10.dp)) {
|
|
||||||
|
if(showAuthPrompt) {
|
||||||
|
AuthorizationPrompt(onSuccess = {
|
||||||
|
showAuthPrompt = false
|
||||||
|
exportAllConfigs() },
|
||||||
|
onError = { error ->
|
||||||
|
showSnackbarMessage(error)
|
||||||
|
showAuthPrompt = false
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
showAuthPrompt = false
|
||||||
|
showSnackbarMessage(context.getString(R.string.authentication_failed))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tunnels.isEmpty()) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.one_tunnel_required),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(15.dp),
|
||||||
|
fontStyle = FontStyle.Italic
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
.clickable(indication = null, interactionSource = interactionSource) {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
tonalElevation = 2.dp,
|
||||||
|
shadowElevation = 2.dp,
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
modifier = (if (WireGuardAutoTunnel.isRunningOnAndroidTv(context))
|
||||||
|
Modifier
|
||||||
|
.height(IntrinsicSize.Min)
|
||||||
|
.fillMaxWidth(fillMaxWidth)
|
||||||
|
.padding(top = 10.dp)
|
||||||
|
else Modifier
|
||||||
|
.fillMaxWidth(fillMaxWidth)
|
||||||
|
.padding(top = 60.dp)).padding(bottom = 25.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier = Modifier.padding(15.dp)
|
||||||
|
) {
|
||||||
|
SectionTitle(title = stringResource(id = R.string.auto_tunneling), padding = screenPadding)
|
||||||
|
ConfigurationToggle(
|
||||||
|
stringResource(id = R.string.tunnel_on_wifi),
|
||||||
|
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
||||||
|
checked = settings.isTunnelOnWifiEnabled,
|
||||||
|
padding = screenPadding,
|
||||||
|
onCheckChanged = {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.onToggleTunnelOnWifi()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.focusRequester(focusRequester)
|
||||||
|
)
|
||||||
|
AnimatedVisibility(visible = settings.isTunnelOnWifiEnabled) {
|
||||||
|
Column {
|
||||||
|
FlowRow(
|
||||||
|
modifier = Modifier.padding(screenPadding),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalArrangement = Arrangement.SpaceEvenly
|
||||||
|
) {
|
||||||
|
trustedSSIDs.forEach { ssid ->
|
||||||
|
ClickableIconButton(
|
||||||
|
onIconClick = {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.onDeleteTrustedSSID(ssid)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
text = ssid,
|
||||||
|
icon = Icons.Filled.Close,
|
||||||
|
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if(trustedSSIDs.isEmpty()) {
|
||||||
|
Text(stringResource(R.string.none), fontStyle = FontStyle.Italic, color = Color.Gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OutlinedTextField(
|
||||||
|
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
||||||
|
value = currentText,
|
||||||
|
onValueChange = { currentText = it },
|
||||||
|
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = screenPadding, top = 5.dp, bottom = 10.dp)
|
||||||
|
.onFocusChanged {
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
|
keyboardController?.hide()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
maxLines = 1,
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
imeAction = ImeAction.Done
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(
|
||||||
|
onDone = {
|
||||||
|
saveTrustedSSID()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = { saveTrustedSSID() }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Add,
|
||||||
|
contentDescription = if (currentText == "") stringResource(id = R.string.trusted_ssid_empty_description) else stringResource(
|
||||||
|
id = R.string.trusted_ssid_value_description
|
||||||
|
),
|
||||||
|
tint = if (currentText == "") Color.Transparent else MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ConfigurationToggle(stringResource(R.string.tunnel_mobile_data),
|
||||||
|
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
||||||
|
checked = settings.isTunnelOnMobileDataEnabled,
|
||||||
|
padding = screenPadding,
|
||||||
|
onCheckChanged = {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.onToggleTunnelOnMobileData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ConfigurationToggle(stringResource(id = R.string.tunnel_on_ethernet),
|
||||||
|
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
||||||
|
checked = settings.isTunnelOnEthernetEnabled,
|
||||||
|
padding = screenPadding,
|
||||||
|
onCheckChanged = {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.onToggleTunnelOnEthernet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ConfigurationToggle(
|
||||||
|
stringResource(R.string.battery_saver),
|
||||||
|
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
||||||
|
checked = settings.isBatterySaverEnabled,
|
||||||
|
padding = screenPadding,
|
||||||
|
onCheckChanged = {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.onToggleBatterySaver()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = 5.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
enabled = !settings.isAlwaysOnVpnEnabled,
|
||||||
|
onClick = {
|
||||||
|
//TODO fix logic for mobile only
|
||||||
|
if(!isAllAutoTunnelPermissionsEnabled() && settings.isTunnelOnWifiEnabled) {
|
||||||
|
val message = if(!isBackgroundLocationGranted) {
|
||||||
|
context.getString(R.string.background_location_required)
|
||||||
|
} else if(viewModel.isLocationServicesNeeded()) {
|
||||||
|
context.getString(R.string.location_services_required)
|
||||||
|
} else {
|
||||||
|
context.getString(R.string.precise_location_required)
|
||||||
|
}
|
||||||
|
showSnackbarMessage(message)
|
||||||
|
} else scope.launch {
|
||||||
|
viewModel.toggleAutoTunnel()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
val autoTunnelButtonText = if(settings.isAutoTunnelEnabled) stringResource(R.string.disable_auto_tunnel)
|
||||||
|
else stringResource(id = R.string.enable_auto_tunnel)
|
||||||
|
Text(autoTunnelButtonText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
|
Surface(
|
||||||
|
tonalElevation = 2.dp,
|
||||||
|
shadowElevation = 2.dp,
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(fillMaxWidth)
|
||||||
|
.height(IntrinsicSize.Min)
|
||||||
|
.padding(bottom = 180.dp)
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier.padding(15.dp)) {
|
modifier = Modifier.padding(15.dp)
|
||||||
SectionTitle(
|
) {
|
||||||
title = stringResource(id = R.string.auto_tunneling),
|
SectionTitle(title = stringResource(id = R.string.other), padding = screenPadding)
|
||||||
padding = screenPadding)
|
ConfigurationToggle(stringResource(R.string.always_on_vpn_support),
|
||||||
ConfigurationToggle(
|
enabled = !settings.isAutoTunnelEnabled,
|
||||||
stringResource(id = R.string.tunnel_on_wifi),
|
checked = settings.isAlwaysOnVpnEnabled,
|
||||||
enabled =
|
padding = screenPadding,
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
onCheckChanged = {
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
scope.launch {
|
||||||
checked = uiState.settings.isTunnelOnWifiEnabled,
|
viewModel.onToggleAlwaysOnVPN()
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = { viewModel.onToggleTunnelOnWifi() },
|
|
||||||
modifier = if(uiState.settings.isAutoTunnelEnabled) Modifier else Modifier.focusRequester(focusRequester).focusProperties { down = focusRequester2 })
|
|
||||||
AnimatedVisibility(visible = uiState.settings.isTunnelOnWifiEnabled) {
|
|
||||||
Column {
|
|
||||||
FlowRow(
|
|
||||||
modifier = Modifier.padding(screenPadding).fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(5.dp)) {
|
|
||||||
uiState.settings.trustedNetworkSSIDs.forEach { ssid ->
|
|
||||||
ClickableIconButton(
|
|
||||||
onClick = { if(WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
|
||||||
viewModel.onDeleteTrustedSSID(ssid)
|
|
||||||
focusRequester2.requestFocus()
|
|
||||||
}},
|
|
||||||
onIconClick = { viewModel.onDeleteTrustedSSID(ssid) },
|
|
||||||
text = ssid,
|
|
||||||
icon = Icons.Filled.Close,
|
|
||||||
enabled =
|
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
|
||||||
uiState.settings.isAlwaysOnVpnEnabled))
|
|
||||||
}
|
|
||||||
if (uiState.settings.trustedNetworkSSIDs.isEmpty()) {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.none),
|
|
||||||
fontStyle = FontStyle.Italic,
|
|
||||||
color = Color.Gray)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OutlinedTextField(
|
|
||||||
enabled =
|
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
|
||||||
value = currentText,
|
|
||||||
onValueChange = { currentText = it },
|
|
||||||
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
|
||||||
modifier =
|
|
||||||
Modifier.padding(
|
|
||||||
start = screenPadding, top = 5.dp, bottom = 10.dp)
|
|
||||||
.focusRequester(focusRequester2)
|
|
||||||
,
|
|
||||||
maxLines = 1,
|
|
||||||
keyboardOptions =
|
|
||||||
KeyboardOptions(
|
|
||||||
capitalization = KeyboardCapitalization.None,
|
|
||||||
imeAction = ImeAction.Done),
|
|
||||||
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
|
|
||||||
trailingIcon = {
|
|
||||||
if (currentText != "") {
|
|
||||||
IconButton(onClick = { saveTrustedSSID() }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Add,
|
|
||||||
contentDescription =
|
|
||||||
if (currentText == "") {
|
|
||||||
stringResource(
|
|
||||||
id = R.string.trusted_ssid_empty_description)
|
|
||||||
} else {
|
|
||||||
stringResource(
|
|
||||||
id = R.string.trusted_ssid_value_description)
|
|
||||||
},
|
|
||||||
tint = MaterialTheme.colorScheme.primary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ConfigurationToggle(
|
|
||||||
stringResource(R.string.tunnel_mobile_data),
|
|
||||||
enabled =
|
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
|
||||||
checked = uiState.settings.isTunnelOnMobileDataEnabled,
|
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = { viewModel.onToggleTunnelOnMobileData() })
|
|
||||||
ConfigurationToggle(
|
|
||||||
stringResource(id = R.string.tunnel_on_ethernet),
|
|
||||||
enabled =
|
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
|
||||||
checked = uiState.settings.isTunnelOnEthernetEnabled,
|
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = { viewModel.onToggleTunnelOnEthernet() })
|
|
||||||
ConfigurationToggle(
|
|
||||||
stringResource(R.string.battery_saver),
|
|
||||||
enabled =
|
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
|
||||||
checked = uiState.settings.isBatterySaverEnabled,
|
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = { viewModel.onToggleBatterySaver() })
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = (if(!uiState.settings.isAutoTunnelEnabled) Modifier else Modifier.focusRequester(focusRequester))
|
|
||||||
.fillMaxSize().padding(top = 5.dp),
|
|
||||||
horizontalArrangement = Arrangement.Center) {
|
|
||||||
TextButton(
|
|
||||||
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
|
|
||||||
onClick = {
|
|
||||||
if (uiState.settings.isTunnelOnWifiEnabled && !uiState.settings.isAutoTunnelEnabled) {
|
|
||||||
when(false) {
|
|
||||||
isBackgroundLocationGranted ->
|
|
||||||
showSnackbarMessage(Event.Error.BackgroundLocationRequired.message)
|
|
||||||
fineLocationState.status.isGranted ->
|
|
||||||
showSnackbarMessage(Event.Error.PreciseLocationRequired.message)
|
|
||||||
viewModel.isLocationEnabled(context) ->
|
|
||||||
showLocationServicesAlertDialog = true
|
|
||||||
else -> {
|
|
||||||
viewModel.toggleAutoTunnel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
viewModel.toggleAutoTunnel()
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
val autoTunnelButtonText =
|
|
||||||
if (uiState.settings.isAutoTunnelEnabled) {
|
|
||||||
stringResource(R.string.disable_auto_tunnel)
|
|
||||||
} else {
|
|
||||||
stringResource(id = R.string.enable_auto_tunnel)
|
|
||||||
}
|
|
||||||
Text(autoTunnelButtonText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (WgQuickBackend.hasKernelSupport()) {
|
|
||||||
Surface(
|
|
||||||
tonalElevation = 2.dp,
|
|
||||||
shadowElevation = 2.dp,
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
color = MaterialTheme.colorScheme.surface,
|
|
||||||
modifier = Modifier.fillMaxWidth(fillMaxWidth).padding(vertical = 10.dp)) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement = Arrangement.Top,
|
|
||||||
modifier = Modifier.padding(15.dp)) {
|
|
||||||
SectionTitle(
|
|
||||||
title = stringResource(id = R.string.kernel), padding = screenPadding)
|
|
||||||
ConfigurationToggle(
|
|
||||||
stringResource(R.string.use_kernel),
|
|
||||||
enabled =
|
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
|
||||||
uiState.settings.isAlwaysOnVpnEnabled ||
|
|
||||||
(uiState.vpnState.status == Tunnel.State.UP)),
|
|
||||||
checked = uiState.settings.isKernelEnabled,
|
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = { viewModel.onToggleKernelMode().let {
|
|
||||||
when(it) {
|
|
||||||
is Result.Error -> showSnackbarMessage(it.error.message)
|
|
||||||
is Result.Success -> {}
|
|
||||||
}
|
|
||||||
} })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
|
||||||
Surface(
|
|
||||||
tonalElevation = 2.dp,
|
|
||||||
shadowElevation = 2.dp,
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
color = MaterialTheme.colorScheme.surface,
|
|
||||||
modifier =
|
|
||||||
Modifier.fillMaxWidth(fillMaxWidth)
|
|
||||||
.padding(vertical = 10.dp)
|
|
||||||
.padding(bottom = 140.dp)) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement = Arrangement.Top,
|
|
||||||
modifier = Modifier.padding(15.dp)) {
|
|
||||||
SectionTitle(
|
|
||||||
title = stringResource(id = R.string.other), padding = screenPadding)
|
|
||||||
ConfigurationToggle(
|
|
||||||
stringResource(R.string.always_on_vpn_support),
|
|
||||||
enabled = !uiState.settings.isAutoTunnelEnabled,
|
|
||||||
checked = uiState.settings.isAlwaysOnVpnEnabled,
|
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = { viewModel.onToggleAlwaysOnVPN() })
|
|
||||||
ConfigurationToggle(
|
|
||||||
stringResource(R.string.enabled_app_shortcuts),
|
|
||||||
enabled = true,
|
|
||||||
checked = uiState.settings.isShortcutsEnabled,
|
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = { viewModel.onToggleShortcutsEnabled() })
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
|
||||||
horizontalArrangement = Arrangement.Center) {
|
|
||||||
TextButton(
|
|
||||||
enabled = !didExportFiles, onClick = { showAuthPrompt = true }) {
|
|
||||||
Text(stringResource(R.string.export_configs))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
ConfigurationToggle(stringResource(R.string.enabled_app_shortcuts),
|
||||||
|
enabled = true,
|
||||||
|
checked = settings.isShortcutsEnabled,
|
||||||
|
padding = screenPadding,
|
||||||
|
onCheckChanged = {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.onToggleShortcutsEnabled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = 5.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
enabled = !didExportFiles,
|
||||||
|
onClick = {
|
||||||
|
showAuthPrompt = true
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.export_configs))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
|
||||||
Spacer(modifier = Modifier.weight(.17f))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
|
Spacer(modifier = Modifier.weight(.17f))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
-13
@@ -1,13 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
|
||||||
|
|
||||||
data class SettingsUiState(
|
|
||||||
val settings : Settings = Settings(),
|
|
||||||
val tunnels : List<TunnelConfig> = emptyList(),
|
|
||||||
val vpnState: VpnState = VpnState(),
|
|
||||||
val isLocationDisclosureShown : Boolean = true,
|
|
||||||
val loading : Boolean = true
|
|
||||||
)
|
|
||||||
+102
-129
@@ -3,171 +3,144 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
import androidx.core.location.LocationManagerCompat
|
import android.os.Build
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.wireguard.android.util.RootShell
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SettingsViewModel
|
class SettingsViewModel @Inject constructor(private val application : Application,
|
||||||
@Inject
|
private val tunnelRepo : TunnelConfigDao, private val settingsRepo : SettingsDoa
|
||||||
constructor(
|
|
||||||
private val application: Application,
|
|
||||||
private val tunnelConfigRepository: TunnelConfigRepository,
|
|
||||||
private val settingsRepository: SettingsRepository,
|
|
||||||
private val dataStoreManager: DataStoreManager,
|
|
||||||
private val rootShell: RootShell,
|
|
||||||
private val vpnService: VpnService
|
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val uiState = combine(
|
private val _trustedSSIDs = MutableStateFlow(emptyList<String>())
|
||||||
settingsRepository.getSettingsFlow(),
|
val trustedSSIDs = _trustedSSIDs.asStateFlow()
|
||||||
tunnelConfigRepository.getTunnelConfigsFlow(),
|
private val _settings = MutableStateFlow(Settings())
|
||||||
vpnService.vpnState,
|
val settings get() = _settings.asStateFlow()
|
||||||
dataStoreManager.locationDisclosureFlow,
|
val tunnels get() = tunnelRepo.getAllFlow()
|
||||||
){ settings, tunnels, tunnelState, locationDisclosure ->
|
init {
|
||||||
SettingsUiState(settings, tunnels, tunnelState, locationDisclosure
|
isLocationServicesEnabled()
|
||||||
?: false, false)
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
}.stateIn(viewModelScope,
|
settingsRepo.getAllFlow().filter { it.isNotEmpty() }.collect {
|
||||||
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), SettingsUiState())
|
val settings = it.first()
|
||||||
|
_settings.emit(settings)
|
||||||
fun onSaveTrustedSSID(ssid: String) : Result<Unit>{
|
_trustedSSIDs.emit(settings.trustedNetworkSSIDs.toList())
|
||||||
val trimmed = ssid.trim()
|
}
|
||||||
return if (!uiState.value.settings.trustedNetworkSSIDs.contains(trimmed)) {
|
|
||||||
uiState.value.settings.trustedNetworkSSIDs.add(trimmed)
|
|
||||||
saveSettings(uiState.value.settings)
|
|
||||||
Result.Success(Unit)
|
|
||||||
} else {
|
|
||||||
Result.Error(Event.Error.SsidConflict)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setLocationDisclosureShown() = viewModelScope.launch {
|
suspend fun onSaveTrustedSSID(ssid: String) {
|
||||||
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, true)
|
val trimmed = ssid.trim()
|
||||||
|
if (!_settings.value.trustedNetworkSSIDs.contains(trimmed)) {
|
||||||
|
_settings.value.trustedNetworkSSIDs.add(trimmed)
|
||||||
|
settingsRepo.save(_settings.value)
|
||||||
|
} else {
|
||||||
|
throw WgTunnelException("SSID already exists.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleTunnelOnMobileData() {
|
suspend fun onToggleTunnelOnMobileData() {
|
||||||
saveSettings(
|
settingsRepo.save(_settings.value.copy(
|
||||||
uiState.value.settings.copy(
|
isTunnelOnMobileDataEnabled = !_settings.value.isTunnelOnMobileDataEnabled
|
||||||
isTunnelOnMobileDataEnabled = !uiState.value.settings.isTunnelOnMobileDataEnabled
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onDeleteTrustedSSID(ssid: String) {
|
|
||||||
saveSettings(uiState.value.settings.copy(
|
|
||||||
trustedNetworkSSIDs = (uiState.value.settings.trustedNetworkSSIDs - ssid).toMutableList()
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getDefaultTunnelOrFirst() : String {
|
suspend fun onDeleteTrustedSSID(ssid: String) {
|
||||||
return uiState.value.settings.defaultTunnel ?: tunnelConfigRepository.getAll().first().toString()
|
_settings.value.trustedNetworkSSIDs.remove(ssid)
|
||||||
|
settingsRepo.save(_settings.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleAutoTunnel() = viewModelScope.launch {
|
private fun emitFirstTunnelAsDefault() = viewModelScope.async {
|
||||||
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
|
_settings.emit(_settings.value.copy(defaultTunnel = getFirstTunnelConfig().toString()))
|
||||||
var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused
|
}
|
||||||
|
|
||||||
if (isAutoTunnelEnabled) {
|
suspend fun toggleAutoTunnel() {
|
||||||
|
if(_settings.value.isAutoTunnelEnabled) {
|
||||||
ServiceManager.stopWatcherService(application)
|
ServiceManager.stopWatcherService(application)
|
||||||
} else {
|
} else {
|
||||||
ServiceManager.startWatcherService(application)
|
if(_settings.value.defaultTunnel == null) {
|
||||||
isAutoTunnelPaused = false
|
emitFirstTunnelAsDefault().await()
|
||||||
|
}
|
||||||
|
val defaultTunnel = _settings.value.defaultTunnel
|
||||||
|
ServiceManager.startWatcherService(application, defaultTunnel!!)
|
||||||
}
|
}
|
||||||
saveSettings(
|
settingsRepo.save(_settings.value.copy(
|
||||||
uiState.value.settings.copy(
|
isAutoTunnelEnabled = !_settings.value.isAutoTunnelEnabled
|
||||||
isAutoTunnelEnabled = !isAutoTunnelEnabled,
|
))
|
||||||
isAutoTunnelPaused = isAutoTunnelPaused,
|
|
||||||
defaultTunnel = getDefaultTunnelOrFirst()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun getFirstTunnelConfig() : TunnelConfig {
|
||||||
|
return tunnelRepo.getAll().first()
|
||||||
|
}
|
||||||
|
|
||||||
fun onToggleAlwaysOnVPN() = viewModelScope.launch {
|
suspend fun onToggleAlwaysOnVPN() {
|
||||||
val updatedSettings = uiState.value.settings.copy(
|
if(_settings.value.defaultTunnel == null) {
|
||||||
isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled,
|
emitFirstTunnelAsDefault().await()
|
||||||
defaultTunnel = getDefaultTunnelOrFirst()
|
}
|
||||||
)
|
val updatedSettings = _settings.value.copy(isAlwaysOnVpnEnabled = !_settings.value.isAlwaysOnVpnEnabled)
|
||||||
|
emitSettings(updatedSettings)
|
||||||
saveSettings(updatedSettings)
|
saveSettings(updatedSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveSettings(settings: Settings) = viewModelScope.launch {
|
private suspend fun emitSettings(settings: Settings) {
|
||||||
settingsRepository.save(settings)
|
_settings.emit(
|
||||||
|
settings
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleTunnelOnEthernet() {
|
private suspend fun saveSettings(settings: Settings) {
|
||||||
saveSettings(uiState.value.settings.copy(
|
settingsRepo.save(settings)
|
||||||
isTunnelOnEthernetEnabled = !uiState.value.settings.isTunnelOnEthernetEnabled
|
}
|
||||||
|
|
||||||
|
suspend fun onToggleTunnelOnEthernet() {
|
||||||
|
if(_settings.value.defaultTunnel == null) {
|
||||||
|
emitFirstTunnelAsDefault().await()
|
||||||
|
}
|
||||||
|
_settings.emit(
|
||||||
|
_settings.value.copy(isTunnelOnEthernetEnabled = !_settings.value.isTunnelOnEthernetEnabled)
|
||||||
|
)
|
||||||
|
settingsRepo.save(_settings.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isLocationServicesEnabled() : Boolean {
|
||||||
|
val locationManager =
|
||||||
|
application.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||||
|
return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isLocationServicesNeeded() : Boolean {
|
||||||
|
return(!isLocationServicesEnabled() && Build.VERSION.SDK_INT > Build.VERSION_CODES.P)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun onToggleShortcutsEnabled() {
|
||||||
|
settingsRepo.save(_settings.value.copy(
|
||||||
|
isShortcutsEnabled = !_settings.value.isShortcutsEnabled
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isLocationEnabled(context: Context): Boolean {
|
suspend fun onToggleBatterySaver() {
|
||||||
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
settingsRepo.save(_settings.value.copy(
|
||||||
return LocationManagerCompat.isLocationEnabled(locationManager)
|
isBatterySaverEnabled = !_settings.value.isBatterySaverEnabled
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleShortcutsEnabled() {
|
suspend fun onToggleTunnelOnWifi() {
|
||||||
saveSettings(
|
settingsRepo.save(_settings.value.copy(
|
||||||
uiState.value.settings.copy(
|
isTunnelOnWifiEnabled = !_settings.value.isTunnelOnWifiEnabled
|
||||||
isShortcutsEnabled = !uiState.value.settings.isShortcutsEnabled
|
))
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onToggleBatterySaver() {
|
|
||||||
saveSettings(
|
|
||||||
uiState.value.settings.copy(
|
|
||||||
isBatterySaverEnabled = !uiState.value.settings.isBatterySaverEnabled
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveKernelMode(on: Boolean) {
|
|
||||||
saveSettings(
|
|
||||||
uiState.value.settings.copy(
|
|
||||||
isKernelEnabled = on
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onToggleTunnelOnWifi() {
|
|
||||||
saveSettings(
|
|
||||||
uiState.value.settings.copy(
|
|
||||||
isTunnelOnWifiEnabled = !uiState.value.settings.isTunnelOnWifiEnabled
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onToggleKernelMode() : Result<Unit> {
|
|
||||||
if (!uiState.value.settings.isKernelEnabled) {
|
|
||||||
try {
|
|
||||||
rootShell.start()
|
|
||||||
Timber.d("Root shell accepted!")
|
|
||||||
saveKernelMode(on = true)
|
|
||||||
} catch (e: RootShell.RootShellException) {
|
|
||||||
saveKernelMode(on = false)
|
|
||||||
return Result.Error(Event.Error.RootDenied)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
saveKernelMode(on = false)
|
|
||||||
}
|
|
||||||
return Result.Success(Unit)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+81
-161
@@ -30,7 +30,6 @@ import androidx.compose.material3.Surface
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
@@ -40,189 +39,110 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.content.ContextCompat.startActivity
|
import androidx.core.content.ContextCompat.startActivity
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SupportScreen(
|
fun SupportScreen(padding : PaddingValues, focusRequester: FocusRequester) {
|
||||||
viewModel: SupportViewModel = hiltViewModel(),
|
|
||||||
padding: PaddingValues,
|
|
||||||
showSnackbarMessage: (String) -> Unit,
|
|
||||||
focusRequester: FocusRequester
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val fillMaxWidth = .85f
|
|
||||||
|
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val context = LocalContext.current
|
||||||
|
val fillMaxWidth = .85f
|
||||||
|
|
||||||
fun openWebPage(url: String) {
|
fun openWebPage(url: String) {
|
||||||
try {
|
val webpage: Uri = Uri.parse(url)
|
||||||
val webpage: Uri = Uri.parse(url)
|
val intent = Intent(Intent.ACTION_VIEW, webpage)
|
||||||
val intent = Intent(Intent.ACTION_VIEW, webpage)
|
context.startActivity(intent)
|
||||||
context.startActivity(intent)
|
}
|
||||||
} catch (e : Exception) {
|
|
||||||
showSnackbarMessage(Event.Error.Exception(e).message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun launchEmail() {
|
fun launchEmail() {
|
||||||
try {
|
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||||
val intent =
|
type = Constants.EMAIL_MIME_TYPE
|
||||||
Intent(Intent.ACTION_SEND).apply {
|
putExtra(Intent.EXTRA_EMAIL, context.getString(R.string.my_email))
|
||||||
type = Constants.EMAIL_MIME_TYPE
|
putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
|
||||||
putExtra(Intent.EXTRA_EMAIL, context.getString(R.string.my_email))
|
}
|
||||||
putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
|
startActivity(context,createChooser(intent, context.getString(R.string.email_chooser)),null)
|
||||||
}
|
}
|
||||||
startActivity(context, createChooser(intent, context.getString(R.string.email_chooser)), null)
|
|
||||||
} catch (e : Exception) {
|
|
||||||
showSnackbarMessage(Event.Error.Exception(e).message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uiState.loading) {
|
Column(horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
LoadingScreen()
|
verticalArrangement = Arrangement.Top,
|
||||||
return
|
modifier = Modifier
|
||||||
}
|
.fillMaxSize()
|
||||||
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Top,
|
|
||||||
modifier =
|
|
||||||
Modifier.fillMaxSize()
|
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.focusable()
|
.focusable()
|
||||||
.padding(padding)) {
|
.padding(padding)) {
|
||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
shadowElevation = 2.dp,
|
shadowElevation = 2.dp,
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
modifier =
|
modifier = (if (WireGuardAutoTunnel.isRunningOnAndroidTv(context))
|
||||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
Modifier
|
||||||
Modifier.height(IntrinsicSize.Min)
|
.height(IntrinsicSize.Min)
|
||||||
.fillMaxWidth(fillMaxWidth)
|
.fillMaxWidth(fillMaxWidth)
|
||||||
.padding(top = 10.dp)
|
.padding(top = 10.dp)
|
||||||
} else {
|
else Modifier
|
||||||
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 20.dp)
|
.fillMaxWidth(fillMaxWidth)
|
||||||
})
|
.padding(top = 20.dp)).padding(bottom = 25.dp)
|
||||||
.padding(bottom = 25.dp)) {
|
) {
|
||||||
Column(modifier = Modifier.padding(20.dp)) {
|
Column(modifier = Modifier.padding(20.dp)) {
|
||||||
Text(
|
Text(stringResource(R.string.thank_you), textAlign = TextAlign.Start, modifier = Modifier.padding(bottom = 20.dp), fontSize = 16.sp)
|
||||||
stringResource(R.string.thank_you),
|
Text(stringResource(id = R.string.support_help_text), textAlign = TextAlign.Start, fontSize = 16.sp, modifier = Modifier.padding(bottom = 20.dp))
|
||||||
textAlign = TextAlign.Start,
|
TextButton(onClick = { openWebPage(context.resources.getString(R.string.docs_url)) }, modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester)) {
|
||||||
fontWeight = FontWeight.Bold,
|
Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||||
modifier = Modifier.padding(bottom = 20.dp),
|
Row {
|
||||||
fontSize = 16.sp)
|
|
||||||
Text(
|
|
||||||
stringResource(id = R.string.support_help_text),
|
|
||||||
textAlign = TextAlign.Start,
|
|
||||||
fontSize = 16.sp,
|
|
||||||
modifier = Modifier.padding(bottom = 20.dp))
|
|
||||||
TextButton(
|
|
||||||
onClick = { openWebPage(context.resources.getString(R.string.docs_url)) },
|
|
||||||
modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester)) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxWidth()) {
|
|
||||||
Row {
|
|
||||||
Icon(Icons.Rounded.Book, stringResource(id = R.string.docs))
|
Icon(Icons.Rounded.Book, stringResource(id = R.string.docs))
|
||||||
Text(
|
Text(stringResource(id = R.string.docs_description), textAlign = TextAlign.Justify, modifier = Modifier.padding(start = 10.dp))
|
||||||
stringResource(id = R.string.docs_description),
|
|
||||||
textAlign = TextAlign.Justify,
|
|
||||||
modifier = Modifier.padding(start = 10.dp))
|
|
||||||
}
|
|
||||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
|
||||||
}
|
}
|
||||||
}
|
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
}
|
||||||
TextButton(
|
}
|
||||||
onClick = { openWebPage(context.resources.getString(R.string.discord_url)) },
|
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||||
modifier = Modifier.padding(vertical = 5.dp)) {
|
TextButton(onClick = { openWebPage(context.resources.getString(R.string.discord_url)) }, modifier = Modifier.padding(vertical = 5.dp)) {
|
||||||
Row(
|
Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
Row {
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
Icon(imageVector = ImageVector.vectorResource(R.drawable.discord), stringResource(
|
||||||
modifier = Modifier.fillMaxWidth()) {
|
id = R.string.discord), Modifier.size(25.dp))
|
||||||
Row {
|
Text(stringResource(id = R.string.discord_description), textAlign = TextAlign.Justify, modifier = Modifier.padding(start = 10.dp))
|
||||||
Icon(
|
|
||||||
imageVector = ImageVector.vectorResource(R.drawable.discord),
|
|
||||||
stringResource(id = R.string.discord),
|
|
||||||
Modifier.size(25.dp))
|
|
||||||
Text(
|
|
||||||
stringResource(id = R.string.discord_description),
|
|
||||||
textAlign = TextAlign.Justify,
|
|
||||||
modifier = Modifier.padding(start = 10.dp))
|
|
||||||
}
|
|
||||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
|
||||||
}
|
}
|
||||||
}
|
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
}
|
||||||
TextButton(
|
}
|
||||||
onClick = { openWebPage(context.resources.getString(R.string.github_url)) },
|
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||||
modifier = Modifier.padding(vertical = 5.dp)) {
|
TextButton(onClick = { openWebPage(context.resources.getString(R.string.github_url)) }, modifier = Modifier.padding(vertical = 5.dp)) {
|
||||||
Row(
|
Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
Row {
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
Icon(imageVector = ImageVector.vectorResource(R.drawable.github), stringResource(
|
||||||
modifier = Modifier.fillMaxWidth()) {
|
id = R.string.github
|
||||||
Row {
|
), Modifier.size(25.dp))
|
||||||
Icon(
|
Text("Open an issue", textAlign = TextAlign.Justify, modifier = Modifier.padding(start = 10.dp))
|
||||||
imageVector = ImageVector.vectorResource(R.drawable.github),
|
|
||||||
stringResource(id = R.string.github),
|
|
||||||
Modifier.size(25.dp))
|
|
||||||
Text(
|
|
||||||
"Open an issue",
|
|
||||||
textAlign = TextAlign.Justify,
|
|
||||||
modifier = Modifier.padding(start = 10.dp))
|
|
||||||
}
|
|
||||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
|
||||||
}
|
}
|
||||||
}
|
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
}
|
||||||
TextButton(
|
}
|
||||||
onClick = { launchEmail() }, modifier = Modifier.padding(vertical = 5.dp)) {
|
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||||
Row(
|
TextButton(onClick = { launchEmail() }, modifier = Modifier.padding(vertical = 5.dp)) {
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
Row {
|
||||||
modifier = Modifier.fillMaxWidth()) {
|
|
||||||
Row {
|
|
||||||
Icon(Icons.Rounded.Mail, stringResource(id = R.string.email))
|
Icon(Icons.Rounded.Mail, stringResource(id = R.string.email))
|
||||||
Text(
|
Text(stringResource(id = R.string.email_description), textAlign = TextAlign.Justify, modifier = Modifier.padding(start = 10.dp))
|
||||||
stringResource(id = R.string.email_description),
|
|
||||||
textAlign = TextAlign.Justify,
|
|
||||||
modifier = Modifier.padding(start = 10.dp))
|
|
||||||
}
|
|
||||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
|
||||||
}
|
}
|
||||||
}
|
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
Text(
|
Text(stringResource(id = R.string.privacy_policy), style = TextStyle(textDecoration = TextDecoration.Underline),
|
||||||
stringResource(id = R.string.privacy_policy),
|
modifier = Modifier.clickable {
|
||||||
style = TextStyle(textDecoration = TextDecoration.Underline),
|
openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
||||||
fontSize = 16.sp,
|
})
|
||||||
modifier =
|
Text("App version: ${BuildConfig.VERSION_NAME}", Modifier.padding(25.dp))
|
||||||
Modifier.clickable {
|
|
||||||
openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
|
||||||
})
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(25.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.padding(25.dp)) {
|
|
||||||
Text("Version: ${BuildConfig.VERSION_NAME}", modifier = Modifier.focusable())
|
|
||||||
Text("Mode: ${if (uiState.settings.isKernelEnabled) "Kernel" else "Userspace" }")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
-8
@@ -1,8 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
|
||||||
|
|
||||||
data class SupportUiState(
|
|
||||||
val settings : Settings = Settings(),
|
|
||||||
val loading : Boolean = true
|
|
||||||
)
|
|
||||||
-25
@@ -1,25 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support
|
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class SupportViewModel @Inject constructor(
|
|
||||||
private val settingsRepository: SettingsRepository
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
val uiState = settingsRepository.getSettingsFlow().map {
|
|
||||||
SupportUiState(it, false)
|
|
||||||
}.stateIn(
|
|
||||||
viewModelScope,
|
|
||||||
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
|
||||||
SupportUiState()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -11,8 +11,7 @@ val Purple40 = Color(0xFF6650a4)
|
|||||||
val PurpleGrey40 = Color(0xFF625b71)
|
val PurpleGrey40 = Color(0xFF625b71)
|
||||||
val Pink40 = Color(0xFFFFFFFF)
|
val Pink40 = Color(0xFFFFFFFF)
|
||||||
|
|
||||||
// status colors
|
//status colors
|
||||||
val brickRed = Color(0xFFCE4257)
|
val brickRed = Color(0xFFCE4257)
|
||||||
val corn = Color(0xFFFBEC5D)
|
|
||||||
val pinkRed = Color(0xFFEF476F)
|
val pinkRed = Color(0xFFEF476F)
|
||||||
val mint = Color(0xFF52B788)
|
val mint = Color(0xFF52B788)
|
||||||
|
|||||||
@@ -15,52 +15,51 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
|
|
||||||
private val DarkColorScheme =
|
private val DarkColorScheme = darkColorScheme(
|
||||||
darkColorScheme(
|
//primary = Purple80,
|
||||||
// primary = Purple80,
|
primary = virdigris,
|
||||||
primary = virdigris,
|
secondary = virdigris,
|
||||||
secondary = virdigris,
|
// secondary = PurpleGrey80,
|
||||||
// secondary = PurpleGrey80,
|
tertiary = virdigris
|
||||||
tertiary = virdigris
|
//tertiary = Pink80
|
||||||
// tertiary = Pink80
|
)
|
||||||
)
|
|
||||||
|
|
||||||
private val LightColorScheme =
|
private val LightColorScheme = lightColorScheme(
|
||||||
lightColorScheme(
|
primary = Purple40,
|
||||||
primary = Purple40,
|
secondary = PurpleGrey40,
|
||||||
secondary = PurpleGrey40,
|
tertiary = Pink40
|
||||||
tertiary = Pink40
|
|
||||||
/* Other default colors to override
|
/* Other default colors to override
|
||||||
background = Color(0xFFFFFBFE),
|
background = Color(0xFFFFFBFE),
|
||||||
surface = Color(0xFFFFFBFE),
|
surface = Color(0xFFFFFBFE),
|
||||||
onPrimary = Color.White,
|
onPrimary = Color.White,
|
||||||
onSecondary = Color.White,
|
onSecondary = Color.White,
|
||||||
onTertiary = Color.White,
|
onTertiary = Color.White,
|
||||||
onBackground = Color(0xFF1C1B1F),
|
onBackground = Color(0xFF1C1B1F),
|
||||||
onSurface = Color(0xFF1C1B1F),
|
onSurface = Color(0xFF1C1B1F),
|
||||||
*/
|
*/
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WireguardAutoTunnelTheme(
|
fun WireguardAutoTunnelTheme(
|
||||||
// force dark theme
|
//force dark theme
|
||||||
darkTheme: Boolean = true,
|
darkTheme : Boolean = true,
|
||||||
// darkTheme: Boolean = isSystemInDarkTheme(),
|
//darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
// Dynamic color is available on Android 12+
|
// Dynamic color is available on Android 12+
|
||||||
// turning off dynamic color for now
|
//turning off dynamic color for now
|
||||||
dynamicColor: Boolean = false,
|
dynamicColor: Boolean = false,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
val colorScheme =
|
|
||||||
when {
|
|
||||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
|
||||||
val context = LocalContext.current
|
|
||||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
darkTheme -> DarkColorScheme
|
val colorScheme = when {
|
||||||
else -> LightColorScheme
|
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||||
|
val context = LocalContext.current
|
||||||
|
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
darkTheme -> DarkColorScheme
|
||||||
|
else -> LightColorScheme
|
||||||
|
}
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
if (!view.isInEditMode) {
|
if (!view.isInEditMode) {
|
||||||
SideEffect {
|
SideEffect {
|
||||||
|
|||||||
@@ -7,30 +7,28 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
// Set of Material typography styles to start with
|
// Set of Material typography styles to start with
|
||||||
val Typography =
|
val Typography = Typography(
|
||||||
Typography(
|
bodyLarge = TextStyle(
|
||||||
bodyLarge =
|
fontFamily = FontFamily.Default,
|
||||||
TextStyle(
|
fontWeight = FontWeight.Normal,
|
||||||
fontFamily = FontFamily.Default,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.Normal,
|
lineHeight = 24.sp,
|
||||||
fontSize = 16.sp,
|
letterSpacing = 0.5.sp
|
||||||
lineHeight = 24.sp,
|
|
||||||
letterSpacing = 0.5.sp
|
|
||||||
)
|
|
||||||
/* Other default text styles to override
|
|
||||||
titleLarge = TextStyle(
|
|
||||||
fontFamily = FontFamily.Default,
|
|
||||||
fontWeight = FontWeight.Normal,
|
|
||||||
fontSize = 22.sp,
|
|
||||||
lineHeight = 28.sp,
|
|
||||||
letterSpacing = 0.sp
|
|
||||||
),
|
|
||||||
labelSmall = TextStyle(
|
|
||||||
fontFamily = FontFamily.Default,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
fontSize = 11.sp,
|
|
||||||
lineHeight = 16.sp,
|
|
||||||
letterSpacing = 0.5.sp
|
|
||||||
)
|
|
||||||
*/
|
|
||||||
)
|
)
|
||||||
|
/* Other default text styles to override
|
||||||
|
titleLarge = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 22.sp,
|
||||||
|
lineHeight = 28.sp,
|
||||||
|
letterSpacing = 0.sp
|
||||||
|
),
|
||||||
|
labelSmall = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
letterSpacing = 0.5.sp
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
)
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.util
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
|
||||||
|
|
||||||
sealed class Event {
|
|
||||||
|
|
||||||
abstract val message: String
|
|
||||||
|
|
||||||
sealed class Error : Event() {
|
|
||||||
data object None : Error() {
|
|
||||||
override val message: String
|
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_none)
|
|
||||||
}
|
|
||||||
data object SsidConflict : Error() {
|
|
||||||
override val message: String
|
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_ssid_exists)
|
|
||||||
}
|
|
||||||
data object RootDenied : Error() {
|
|
||||||
override val message: String
|
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_root_denied)
|
|
||||||
}
|
|
||||||
data class General(val customMessage: String) : Error() {
|
|
||||||
override val message: String
|
|
||||||
get() = customMessage
|
|
||||||
}
|
|
||||||
data class Exception(val exception : kotlin.Exception) : Error() {
|
|
||||||
override val message: String
|
|
||||||
get() = exception.message ?: WireGuardAutoTunnel.instance.getString(R.string.unknown_error)
|
|
||||||
}
|
|
||||||
data object InvalidQrCode : Error() {
|
|
||||||
override val message: String
|
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_invalid_code)
|
|
||||||
}
|
|
||||||
data object InvalidFileExtension : Error() {
|
|
||||||
override val message: String
|
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
|
|
||||||
}
|
|
||||||
data object FileReadFailed : Error() {
|
|
||||||
override val message: String
|
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
|
|
||||||
}
|
|
||||||
data object AuthenticationFailed : Error() {
|
|
||||||
override val message: String
|
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authentication_failed)
|
|
||||||
}
|
|
||||||
data object AuthorizationFailed : Error() {
|
|
||||||
override val message: String
|
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authorization_failed)
|
|
||||||
}
|
|
||||||
data object BackgroundLocationRequired : Error() {
|
|
||||||
override val message: String
|
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.background_location_required)
|
|
||||||
}
|
|
||||||
data object LocationServicesRequired : Error() {
|
|
||||||
override val message: String
|
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.location_services_required)
|
|
||||||
}
|
|
||||||
data object PreciseLocationRequired : Error() {
|
|
||||||
override val message: String
|
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.precise_location_required)
|
|
||||||
}
|
|
||||||
data object FileExplorerRequired : Error() {
|
|
||||||
override val message: String
|
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_no_file_explorer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sealed class Message : Event() {
|
|
||||||
data object ConfigSaved: Message() {
|
|
||||||
override val message: String
|
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.config_changes_saved)
|
|
||||||
}
|
|
||||||
data object ConfigsExported: Message() {
|
|
||||||
override val message: String
|
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.exported_configs_message)
|
|
||||||
}
|
|
||||||
data object TunnelOffAction: Message() {
|
|
||||||
override val message: String
|
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_tunnel)
|
|
||||||
}
|
|
||||||
data object TunnelOnAction: Message() {
|
|
||||||
override val message: String
|
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_on_tunnel)
|
|
||||||
}
|
|
||||||
data object AutoTunnelOffAction: Message() {
|
|
||||||
override val message: String
|
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_auto)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.util
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.pm.PackageInfo
|
|
||||||
import com.wireguard.android.backend.Statistics
|
|
||||||
import com.wireguard.android.backend.Statistics.PeerStats
|
|
||||||
import com.wireguard.crypto.Key
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.math.BigDecimal
|
|
||||||
import java.text.DecimalFormat
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
|
||||||
|
|
||||||
fun BroadcastReceiver.goAsync(
|
|
||||||
context: CoroutineContext = EmptyCoroutineContext,
|
|
||||||
block: suspend CoroutineScope.() -> Unit
|
|
||||||
) {
|
|
||||||
val pendingResult = goAsync()
|
|
||||||
@OptIn(DelicateCoroutinesApi::class) // Must run globally; there's no teardown callback.
|
|
||||||
GlobalScope.launch(context) {
|
|
||||||
try {
|
|
||||||
block()
|
|
||||||
} finally {
|
|
||||||
pendingResult.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun BigDecimal.toThreeDecimalPlaceString(): String {
|
|
||||||
val df = DecimalFormat("#.###")
|
|
||||||
return df.format(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> List<T>.update(index: Int, item: T): List<T> = toMutableList().apply { this[index] = item }
|
|
||||||
fun <T> List<T>.removeAt(index: Int): List<T> = toMutableList().apply { this.removeAt(index) }
|
|
||||||
|
|
||||||
typealias TunnelConfigs = List<TunnelConfig>
|
|
||||||
typealias Packages = List<PackageInfo>
|
|
||||||
|
|
||||||
fun Statistics.mapPeerStats(): Map<Key, PeerStats?> {
|
|
||||||
return this.peers().associateWith { key ->
|
|
||||||
(this.peer(key))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun PeerStats.latestHandshakeSeconds() : Long? {
|
|
||||||
return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun PeerStats.handshakeStatus() : HandshakeStatus {
|
|
||||||
//TODO add never connected status after duration
|
|
||||||
return this.latestHandshakeSeconds().let {
|
|
||||||
when {
|
|
||||||
it == null -> HandshakeStatus.NOT_STARTED
|
|
||||||
it <= HandshakeStatus.STALE_TIME_LIMIT_SEC -> HandshakeStatus.HEALTHY
|
|
||||||
it > HandshakeStatus.STALE_TIME_LIMIT_SEC -> HandshakeStatus.STALE
|
|
||||||
else -> {
|
|
||||||
HandshakeStatus.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -6,23 +6,24 @@ import java.time.Instant
|
|||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
|
|
||||||
object NumberUtils {
|
object NumberUtils {
|
||||||
|
|
||||||
private const val BYTES_IN_KB = 1024.0
|
private const val BYTES_IN_KB = 1024.0
|
||||||
private val BYTES_IN_MB = BYTES_IN_KB.pow(2.0)
|
private val BYTES_IN_MB = BYTES_IN_KB.pow(2.0)
|
||||||
private val keyValidationRegex = """^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=${'$'}""".toRegex()
|
private val keyValidationRegex = """^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=${'$'}""".toRegex()
|
||||||
|
|
||||||
fun bytesToMB(bytes: Long): BigDecimal {
|
fun bytesToMB(bytes : Long) : BigDecimal {
|
||||||
return bytes.toBigDecimal().divide(BYTES_IN_MB.toBigDecimal())
|
return bytes.toBigDecimal().divide(BYTES_IN_MB.toBigDecimal())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isValidKey(key: String): Boolean {
|
fun isValidKey(key : String) : Boolean {
|
||||||
return key.matches(keyValidationRegex)
|
return key.matches(keyValidationRegex)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun generateRandomTunnelName(): String {
|
fun generateRandomTunnelName() : String {
|
||||||
return "tunnel${(Math.random() * 100000).toInt()}"
|
return "tunnel${(Math.random() * 100000).toInt()}"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSecondsBetweenTimestampAndNow(epoch: Long): Long? {
|
fun getSecondsBetweenTimestampAndNow(epoch : Long) : Long? {
|
||||||
return if (epoch != 0L) {
|
return if (epoch != 0L) {
|
||||||
val time = Instant.ofEpochMilli(epoch)
|
val time = Instant.ofEpochMilli(epoch)
|
||||||
return Duration.between(time, Instant.now()).seconds
|
return Duration.between(time, Instant.now()).seconds
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.util
|
|
||||||
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
|
|
||||||
sealed class Result<T> {
|
|
||||||
class Success<T>(val data: T): Result<T>()
|
|
||||||
class Error<T>(val error : Event.Error): Result<T>() {
|
|
||||||
init {
|
|
||||||
when(this.error) {
|
|
||||||
is Event.Error.Exception -> Timber.e(this.error.exception)
|
|
||||||
else -> Timber.e(this.error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+16
-28
@@ -6,55 +6,43 @@ import android.os.Build
|
|||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.provider.MediaStore.MediaColumns
|
import android.provider.MediaStore.MediaColumns
|
||||||
|
import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
object FileUtils {
|
object StorageUtil {
|
||||||
private const val ZIP_FILE_MIME_TYPE = "application/zip"
|
private const val ZIP_FILE_MIME_TYPE = "application/zip"
|
||||||
|
private fun createDownloadsFileOutputStream(context: Context, fileName: String, mimeType : String = Constants.ALLOWED_FILE_TYPES) : OutputStream? {
|
||||||
private fun createDownloadsFileOutputStream(
|
|
||||||
context: Context,
|
|
||||||
fileName: String,
|
|
||||||
mimeType: String = Constants.ALLOWED_FILE_TYPES
|
|
||||||
): OutputStream? {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
val resolver = context.contentResolver
|
val resolver = context.contentResolver
|
||||||
val contentValues =
|
val contentValues = ContentValues().apply {
|
||||||
ContentValues().apply {
|
put(MediaColumns.DISPLAY_NAME, fileName)
|
||||||
put(MediaColumns.DISPLAY_NAME, fileName)
|
put(MediaColumns.MIME_TYPE, mimeType)
|
||||||
put(MediaColumns.MIME_TYPE, mimeType)
|
put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||||
put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
}
|
||||||
}
|
|
||||||
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
|
|
||||||
return resolver.openOutputStream(uri)
|
return resolver.openOutputStream(uri)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val target =
|
val target = File(
|
||||||
File(
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
fileName
|
||||||
fileName
|
)
|
||||||
)
|
|
||||||
return target.outputStream()
|
return target.outputStream()
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveFilesToZip(
|
fun saveFilesToZip(context: Context, files : List<File>) {
|
||||||
context: Context,
|
val zipOutputStream = createDownloadsFileOutputStream(context, "wg-export_${Instant.now().epochSecond}.zip", ZIP_FILE_MIME_TYPE)
|
||||||
files: List<File>
|
|
||||||
) {
|
|
||||||
val zipOutputStream = createDownloadsFileOutputStream(
|
|
||||||
context,
|
|
||||||
"wg-export_${Instant.now().epochSecond}.zip",
|
|
||||||
ZIP_FILE_MIME_TYPE
|
|
||||||
)
|
|
||||||
ZipOutputStream(zipOutputStream).use { zos ->
|
ZipOutputStream(zipOutputStream).use { zos ->
|
||||||
files.forEach { file ->
|
files.forEach { file ->
|
||||||
val entry = ZipEntry(file.name)
|
val entry = ZipEntry( file.name)
|
||||||
zos.putNextEntry(entry)
|
zos.putNextEntry(entry)
|
||||||
if (file.isFile) {
|
if (file.isFile) {
|
||||||
file.inputStream().use { fis -> fis.copyTo(zos) }
|
file.inputStream().use { fis -> fis.copyTo(zos) }
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.util
|
||||||
|
|
||||||
|
import com.wireguard.config.BadConfigException
|
||||||
|
|
||||||
|
class WgTunnelException(e: Exception) : Exception() {
|
||||||
|
constructor(message : String) : this(Exception(message))
|
||||||
|
|
||||||
|
override val message: String = generateExceptionMessage(e)
|
||||||
|
private fun generateExceptionMessage(e : Exception) : String {
|
||||||
|
return when(e) {
|
||||||
|
is BadConfigException -> "${e.section.name} ${e.location.name} ${e.reason.name}"
|
||||||
|
else -> e.message ?: "Unknown error occurred"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,14 +9,13 @@
|
|||||||
<string name="github_url">https://github.com/zaneschepke/wgtunnel/issues</string>
|
<string name="github_url">https://github.com/zaneschepke/wgtunnel/issues</string>
|
||||||
<string name="docs_url">https://zaneschepke.com/wgtunnel-docs/overview.html</string>
|
<string name="docs_url">https://zaneschepke.com/wgtunnel-docs/overview.html</string>
|
||||||
<string name="privacy_policy_url">https://zaneschepke.com/wgtunnel-docs/privacypolicy.html</string>
|
<string name="privacy_policy_url">https://zaneschepke.com/wgtunnel-docs/privacypolicy.html</string>
|
||||||
<string name="error_file_extension">File is not a .conf or .zip</string>
|
<string name="file_extension_message">File is not a .conf or .zip</string>
|
||||||
<string name="turn_off_tunnel">Action requires tunnel off</string>
|
<string name="turn_off_tunnel">Turn off tunnel before editing</string>
|
||||||
<string name="no_tunnels">No tunnels added yet!</string>
|
<string name="no_tunnels">No tunnels added yet!</string>
|
||||||
<string name="tunnel_exists">Tunnel name already exists</string>
|
<string name="tunnel_exists">Tunnel name already exists</string>
|
||||||
<string name="discord_url">https://discord.gg/rbRRNh6H7V</string>
|
<string name="discord_url">https://discord.gg/rbRRNh6H7V</string>
|
||||||
<string name="watcher_notification_title">Watcher Service</string>
|
<string name="watcher_notification_title">Watcher Service</string>
|
||||||
<string name="watcher_notification_text_active">Monitoring network state changes: active</string>
|
<string name="watcher_notification_text">Monitoring network state changes</string>
|
||||||
<string name="watcher_notification_text_paused">Monitoring network state changes: paused</string>
|
|
||||||
<string name="tunnel_start_title">VPN Connected</string>
|
<string name="tunnel_start_title">VPN Connected</string>
|
||||||
<string name="tunnel_start_text">Connected to tunnel -</string>
|
<string name="tunnel_start_text">Connected to tunnel -</string>
|
||||||
<string name="vpn_permission_required">VPN permission is required for the app to work properly. If this permission is not launching, please disable \"Always-on VPN\" in your phone settings for the official WireGuard mobile app and try again.</string>
|
<string name="vpn_permission_required">VPN permission is required for the app to work properly. If this permission is not launching, please disable \"Always-on VPN\" in your phone settings for the official WireGuard mobile app and try again.</string>
|
||||||
@@ -79,7 +78,7 @@
|
|||||||
<string name="lost_connection_failure_message">Attempting to reconnect to server after more than one minute of no response.</string>
|
<string name="lost_connection_failure_message">Attempting to reconnect to server after more than one minute of no response.</string>
|
||||||
<string name="always_on_vpn_support">Allow Always-On VPN </string>
|
<string name="always_on_vpn_support">Allow Always-On VPN </string>
|
||||||
<string name="select_tunnel_message">Please select a tunnel first</string>
|
<string name="select_tunnel_message">Please select a tunnel first</string>
|
||||||
<string name="location_services_not_detected">Location Services Not Detected</string>
|
<string name="location_services_not_detected">Unable to detect Location Services which are required for this feature. Please enable Location Services.</string>
|
||||||
<string name="check_again">Check again</string>
|
<string name="check_again">Check again</string>
|
||||||
<string name="detecting_location_services_disabled">Detecting Location Services disabled</string>
|
<string name="detecting_location_services_disabled">Detecting Location Services disabled</string>
|
||||||
<string name="precise_location_message">This feature requires precise location to access Wi-Fi SSID name. Please enable precise location here or in the app settings.</string>
|
<string name="precise_location_message">This feature requires precise location to access Wi-Fi SSID name. Please enable precise location here or in the app settings.</string>
|
||||||
@@ -97,6 +96,8 @@
|
|||||||
<string name="none">No trusted wifi names</string>
|
<string name="none">No trusted wifi names</string>
|
||||||
<string name="never">Never</string>
|
<string name="never">Never</string>
|
||||||
<string name="stream_failed">Failed to open file stream.</string>
|
<string name="stream_failed">Failed to open file stream.</string>
|
||||||
|
<string name="unknown_error_message">An unknown error occurred.</string>
|
||||||
|
<string name="no_file_app">No file app installed.</string>
|
||||||
<string name="other">Other</string>
|
<string name="other">Other</string>
|
||||||
<string name="auto_tunneling">Auto-tunneling</string>
|
<string name="auto_tunneling">Auto-tunneling</string>
|
||||||
<string name="select_tunnel">Select tunnel to use</string>
|
<string name="select_tunnel">Select tunnel to use</string>
|
||||||
@@ -107,7 +108,6 @@
|
|||||||
<string name="create_import">Create from scratch</string>
|
<string name="create_import">Create from scratch</string>
|
||||||
<string name="set_primary">Set primary</string>
|
<string name="set_primary">Set primary</string>
|
||||||
<string name="turn_off_auto">Action requires auto-tunnel disabled</string>
|
<string name="turn_off_auto">Action requires auto-tunnel disabled</string>
|
||||||
<string name="turn_on_tunnel">Action requires active tunnel</string>
|
|
||||||
<string name="add_peer">Add peer</string>
|
<string name="add_peer">Add peer</string>
|
||||||
<string name="info">Info</string>
|
<string name="info">Info</string>
|
||||||
<string name="done">Done</string>
|
<string name="done">Done</string>
|
||||||
@@ -128,8 +128,7 @@
|
|||||||
<string name="cancel">Cancel</string>
|
<string name="cancel">Cancel</string>
|
||||||
<string name="primary_tunnel_change">Primary tunnel change</string>
|
<string name="primary_tunnel_change">Primary tunnel change</string>
|
||||||
<string name="primary_tunnel_change_question">Would you like to make this your primary tunnel?</string>
|
<string name="primary_tunnel_change_question">Would you like to make this your primary tunnel?</string>
|
||||||
<string name="error_authentication_failed">Authentication failed</string>
|
<string name="authentication_failed">Authentication failed</string>
|
||||||
<string name="error_authorization_failed">Failed to authorize</string>
|
|
||||||
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
||||||
<string name="export_configs">Export configs</string>
|
<string name="export_configs">Export configs</string>
|
||||||
<string name="battery_saver">Battery saver (beta)</string>
|
<string name="battery_saver">Battery saver (beta)</string>
|
||||||
@@ -138,6 +137,7 @@
|
|||||||
<string name="precise_location_required">Precise location required</string>
|
<string name="precise_location_required">Precise location required</string>
|
||||||
<string name="unknown_error">Unknown error occurred</string>
|
<string name="unknown_error">Unknown error occurred</string>
|
||||||
<string name="exported_configs_message">Exported configs to downloads</string>
|
<string name="exported_configs_message">Exported configs to downloads</string>
|
||||||
|
<string name="no_file_explorer">No file explorer installed</string>
|
||||||
<string name="status">status</string>
|
<string name="status">status</string>
|
||||||
<string name="tunnel_on_wifi">Tunnel on untrusted wifi</string>
|
<string name="tunnel_on_wifi">Tunnel on untrusted wifi</string>
|
||||||
<string name="my_email">zanecschepke@gmail.com</string>
|
<string name="my_email">zanecschepke@gmail.com</string>
|
||||||
@@ -152,16 +152,5 @@
|
|||||||
<string name="email">Email</string>
|
<string name="email">Email</string>
|
||||||
<string name="email_description">Send me an email</string>
|
<string name="email_description">Send me an email</string>
|
||||||
<string name="support_help_text">If you are experiencing issues, have improvement ideas, or just want to engage, the following resources are available:</string>
|
<string name="support_help_text">If you are experiencing issues, have improvement ideas, or just want to engage, the following resources are available:</string>
|
||||||
<string name="kernel">Kernel</string>
|
|
||||||
<string name="use_kernel">Use kernel module</string>
|
|
||||||
<string name="error_ssid_exists">SSID already exists</string>
|
|
||||||
<string name="error_root_denied">Root shell denied</string>
|
|
||||||
<string name="error_no_file_explorer">No file explorer installed</string>
|
|
||||||
<string name="error_no_scan">No code scanned</string>
|
|
||||||
<string name="error_invalid_code">Invalid QR code</string>
|
|
||||||
<string name="error_none">No error</string>
|
|
||||||
<string name="error_file_read">Failed to read file</string>
|
|
||||||
<string name="location_service_missing">Location Services Not Detected</string>
|
|
||||||
<string name="location_services_missing_message">The app is not detecting any location services enabled on your device. Depending on the device, this could cause the untrusted wifi feature to fail to read the wifi name. Would you like to continue anyways?</string>
|
|
||||||
<string name="auto_tunnel_title">Auto-tunnel Service</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
+1
-1
@@ -12,5 +12,5 @@ plugins {
|
|||||||
alias(libs.plugins.kotlin.android) apply false
|
alias(libs.plugins.kotlin.android) apply false
|
||||||
alias(libs.plugins.hilt.android) apply false
|
alias(libs.plugins.hilt.android) apply false
|
||||||
kotlin("plugin.serialization").version(libs.versions.kotlin).apply(false)
|
kotlin("plugin.serialization").version(libs.versions.kotlin).apply(false)
|
||||||
alias(libs.plugins.ksp) apply false
|
alias(libs.plugins.ksp) apply false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,29 @@
|
|||||||
import org.gradle.api.invocation.Gradle
|
import org.gradle.api.invocation.Gradle
|
||||||
|
|
||||||
object BuildHelper {
|
object BuildHelper {
|
||||||
private fun getCurrentFlavor(gradle: Gradle): String {
|
private fun getCurrentFlavor(gradle : Gradle): String {
|
||||||
val taskRequestsStr = gradle.startParameter.taskRequests.toString()
|
val taskRequestsStr = gradle.startParameter.taskRequests.toString()
|
||||||
val pattern: java.util.regex.Pattern =
|
val pattern: java.util.regex.Pattern = if (taskRequestsStr.contains("assemble")) {
|
||||||
if (taskRequestsStr.contains("assemble")) {
|
java.util.regex.Pattern.compile("assemble(\\w+)(Release|Debug)")
|
||||||
java.util.regex.Pattern.compile("assemble(\\w+)(Release|Debug)")
|
} else {
|
||||||
} else {
|
java.util.regex.Pattern.compile("bundle(\\w+)(Release|Debug)")
|
||||||
java.util.regex.Pattern.compile("bundle(\\w+)(Release|Debug)")
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val matcher = pattern.matcher(taskRequestsStr)
|
val matcher = pattern.matcher(taskRequestsStr)
|
||||||
val flavor =
|
val flavor = if (matcher.find()) {
|
||||||
if (matcher.find()) {
|
matcher.group(1).lowercase()
|
||||||
matcher.group(1).lowercase()
|
} else {
|
||||||
} else {
|
print("NO FLAVOR FOUND")
|
||||||
print("NO FLAVOR FOUND")
|
""
|
||||||
""
|
}
|
||||||
}
|
|
||||||
return flavor
|
return flavor
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isGeneralFlavor(gradle: Gradle): Boolean {
|
fun isGeneralFlavor(gradle : Gradle) : Boolean {
|
||||||
return getCurrentFlavor(gradle) == "general"
|
return getCurrentFlavor(gradle) == "general"
|
||||||
}
|
}
|
||||||
|
fun isReleaseBuild(gradle: Gradle) : Boolean {
|
||||||
fun isReleaseBuild(gradle: Gradle): Boolean {
|
return (gradle.startParameter.taskNames.size > 0 && gradle.startParameter.taskNames[0].contains(
|
||||||
return (
|
"Release"))
|
||||||
gradle.startParameter.taskNames.size > 0 &&
|
|
||||||
gradle.startParameter.taskNames[0].contains(
|
|
||||||
"Release",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
object Constants {
|
object Constants {
|
||||||
const val VERSION_NAME = "3.3.2"
|
const val VERSION_NAME = "3.2.3"
|
||||||
const val JVM_TARGET = "17"
|
const val JVM_TARGET = "17"
|
||||||
const val VERSION_CODE = 33200
|
const val VERSION_CODE = 32300
|
||||||
const val TARGET_SDK = 34
|
const val TARGET_SDK = 34
|
||||||
const val MIN_SDK = 26
|
const val MIN_SDK = 26
|
||||||
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
Enhancements:
|
|
||||||
- Add basic WireGuard Kernel support
|
|
||||||
- Improved location disclosure flow
|
|
||||||
- Fix auto-tunnel permissions bug
|
|
||||||
- Various other UI bug fixes
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
Fixes:
|
|
||||||
- Android 14 foreground permissions
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
Enhancements:
|
|
||||||
- Refactor state management
|
|
||||||
- Improve AndroidTV navigation
|
|
||||||
- Improve auto-tunneling efficiency
|
|
||||||
- Improve navigation
|
|
||||||
- Auto-tunneling pause feature
|
|
||||||
- Many bugfixes
|
|
||||||
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
Enhancements:
|
|
||||||
- Refactor state management
|
|
||||||
- Improve AndroidTV navigation
|
|
||||||
- Improve auto-tunneling efficiency
|
|
||||||
- Improve navigation
|
|
||||||
- Auto-tunneling pause feature
|
|
||||||
- Many bugfixes
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
Enhancements:
|
|
||||||
- Refactor state management
|
|
||||||
- Improve AndroidTV navigation
|
|
||||||
- Improve auto-tunneling efficiency
|
|
||||||
- Improve navigation
|
|
||||||
- Auto-tunneling pause feature
|
|
||||||
- Fix foreground start auto-tunnel
|
|
||||||
- Many bugfixes
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user