fix: tunnel disable frozen
Fixes a bug where after toggling a tunnel so many times it would eventually get stuck in the on position. This was also impacting auto-tunneling reliability. Fixes a bug where clicking the email button on the support page would not populate the "to" email field. Fixes a bug where you could not save a tunnel without having configured DNS. Added a dialog to prompt user if they are deleting a tunnel. Added battery optimization disable request when first launching auto-tunneling. Format to kotlinlang standards. Fix ci google play deploy. Closes #63
This commit is contained in:
parent
7ec294b789
commit
5a15776bb3
|
@ -0,0 +1,85 @@
|
||||||
|
[{*.kt,*.kts}]
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
max_line_length = 100
|
||||||
|
indent_size = 4
|
||||||
|
ij_continuation_indent_size = 4
|
||||||
|
ij_java_names_count_to_use_import_on_demand = 9999
|
||||||
|
ij_kotlin_align_in_columns_case_branch = false
|
||||||
|
ij_kotlin_align_multiline_binary_operation = false
|
||||||
|
ij_kotlin_align_multiline_extends_list = false
|
||||||
|
ij_kotlin_align_multiline_method_parentheses = false
|
||||||
|
ij_kotlin_align_multiline_parameters = true
|
||||||
|
ij_kotlin_align_multiline_parameters_in_calls = false
|
||||||
|
ij_kotlin_allow_trailing_comma = true
|
||||||
|
ij_kotlin_allow_trailing_comma_on_call_site = true
|
||||||
|
ij_kotlin_assignment_wrap = normal
|
||||||
|
ij_kotlin_blank_lines_after_class_header = 0
|
||||||
|
ij_kotlin_blank_lines_around_block_when_branches = 0
|
||||||
|
ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1
|
||||||
|
ij_kotlin_block_comment_at_first_column = true
|
||||||
|
ij_kotlin_call_parameters_new_line_after_left_paren = true
|
||||||
|
ij_kotlin_call_parameters_right_paren_on_new_line = false
|
||||||
|
ij_kotlin_call_parameters_wrap = on_every_item
|
||||||
|
ij_kotlin_catch_on_new_line = false
|
||||||
|
ij_kotlin_class_annotation_wrap = split_into_lines
|
||||||
|
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
|
||||||
|
ij_kotlin_continuation_indent_for_chained_calls = true
|
||||||
|
ij_kotlin_continuation_indent_for_expression_bodies = true
|
||||||
|
ij_kotlin_continuation_indent_in_argument_lists = true
|
||||||
|
ij_kotlin_continuation_indent_in_elvis = false
|
||||||
|
ij_kotlin_continuation_indent_in_if_conditions = false
|
||||||
|
ij_kotlin_continuation_indent_in_parameter_lists = false
|
||||||
|
ij_kotlin_continuation_indent_in_supertype_lists = false
|
||||||
|
ij_kotlin_else_on_new_line = false
|
||||||
|
ij_kotlin_enum_constants_wrap = off
|
||||||
|
ij_kotlin_extends_list_wrap = normal
|
||||||
|
ij_kotlin_field_annotation_wrap = split_into_lines
|
||||||
|
ij_kotlin_finally_on_new_line = false
|
||||||
|
ij_kotlin_if_rparen_on_new_line = false
|
||||||
|
ij_kotlin_import_nested_classes = false
|
||||||
|
ij_kotlin_insert_whitespaces_in_simple_one_line_method = true
|
||||||
|
ij_kotlin_keep_blank_lines_before_right_brace = 2
|
||||||
|
ij_kotlin_keep_blank_lines_in_code = 2
|
||||||
|
ij_kotlin_keep_blank_lines_in_declarations = 2
|
||||||
|
ij_kotlin_keep_first_column_comment = true
|
||||||
|
ij_kotlin_keep_indents_on_empty_lines = false
|
||||||
|
ij_kotlin_keep_line_breaks = true
|
||||||
|
ij_kotlin_lbrace_on_next_line = false
|
||||||
|
ij_kotlin_line_comment_add_space = false
|
||||||
|
ij_kotlin_line_comment_at_first_column = true
|
||||||
|
ij_kotlin_method_annotation_wrap = split_into_lines
|
||||||
|
ij_kotlin_method_call_chain_wrap = normal
|
||||||
|
ij_kotlin_method_parameters_new_line_after_left_paren = true
|
||||||
|
ij_kotlin_method_parameters_right_paren_on_new_line = true
|
||||||
|
ij_kotlin_method_parameters_wrap = on_every_item
|
||||||
|
ij_kotlin_name_count_to_use_star_import = 9999
|
||||||
|
ij_kotlin_name_count_to_use_star_import_for_members = 9999
|
||||||
|
ij_kotlin_parameter_annotation_wrap = off
|
||||||
|
ij_kotlin_space_after_comma = true
|
||||||
|
ij_kotlin_space_after_extend_colon = true
|
||||||
|
ij_kotlin_space_after_type_colon = true
|
||||||
|
ij_kotlin_space_before_catch_parentheses = true
|
||||||
|
ij_kotlin_space_before_comma = false
|
||||||
|
ij_kotlin_space_before_extend_colon = true
|
||||||
|
ij_kotlin_space_before_for_parentheses = true
|
||||||
|
ij_kotlin_space_before_if_parentheses = true
|
||||||
|
ij_kotlin_space_before_lambda_arrow = true
|
||||||
|
ij_kotlin_space_before_type_colon = false
|
||||||
|
ij_kotlin_space_before_when_parentheses = true
|
||||||
|
ij_kotlin_space_before_while_parentheses = true
|
||||||
|
ij_kotlin_spaces_around_additive_operators = true
|
||||||
|
ij_kotlin_spaces_around_assignment_operators = true
|
||||||
|
ij_kotlin_spaces_around_equality_operators = true
|
||||||
|
ij_kotlin_spaces_around_function_type_arrow = true
|
||||||
|
ij_kotlin_spaces_around_logical_operators = true
|
||||||
|
ij_kotlin_spaces_around_multiplicative_operators = true
|
||||||
|
ij_kotlin_spaces_around_range = false
|
||||||
|
ij_kotlin_spaces_around_relational_operators = true
|
||||||
|
ij_kotlin_spaces_around_unary_operator = false
|
||||||
|
ij_kotlin_spaces_around_when_arrow = true
|
||||||
|
ij_kotlin_variable_annotation_wrap = off
|
||||||
|
ij_kotlin_while_on_new_line = false
|
||||||
|
ij_kotlin_wrap_elvis_expressions = 1
|
||||||
|
ij_kotlin_wrap_expression_body_functions = 1
|
||||||
|
ij_kotlin_wrap_first_method_in_call_chain = false
|
|
@ -11,12 +11,14 @@ assignees: zaneschepke
|
||||||
A clear and concise description of what the bug is.
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
**Smartphone (please complete the following information):**
|
**Smartphone (please complete the following information):**
|
||||||
- Device: [e.g. iPhone6]
|
|
||||||
- Android Version: [e.g. iOS8.1]
|
- Device: [e.g. Pixel 4a]
|
||||||
- App Version [e.g. 22]
|
- Android Version: [e.g. Android 13]
|
||||||
|
- App Version [e.g. 3.3.3]
|
||||||
|
|
||||||
**To Reproduce**
|
**To Reproduce**
|
||||||
Steps to reproduce the behavior:
|
Steps to reproduce the behavior:
|
||||||
|
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Click on '....'
|
2. Click on '....'
|
||||||
3. Scroll down to '....'
|
3. Scroll down to '....'
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Support
|
# Support
|
||||||
|
|
||||||
If you are experiencing issues with the app, the following resources are available to help you.
|
If you are experiencing issues with the app, the following resources are available to help you.
|
||||||
|
|
||||||
<ol>
|
<ol>
|
||||||
<li>
|
<li>
|
||||||
|
|
|
@ -9,7 +9,7 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build Signed APK
|
name: Build Signed APK
|
||||||
# change to macos because of hilt issues on ubuntu in gradle 8.3
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
@ -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/33300.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
|
||||||
|
|
11
README.md
11
README.md
|
@ -28,7 +28,10 @@ WG Tunnel
|
||||||
|
|
||||||
<div align="left">
|
<div align="left">
|
||||||
|
|
||||||
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) with added features. Built using the [wireguard-android](https://github.com/WireGuard/wireguard-android) library and [Jetpack Compose](https://developer.android.com/jetpack/compose), this application was inspired by the official [WireGuard Android](https://github.com/WireGuard/wireguard-android) app.
|
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) with added
|
||||||
|
features. Built using the [wireguard-android](https://github.com/WireGuard/wireguard-android)
|
||||||
|
library and [Jetpack Compose](https://developer.android.com/jetpack/compose), this application was
|
||||||
|
inspired by the official [WireGuard Android](https://github.com/WireGuard/wireguard-android) app.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -47,7 +50,8 @@ This is an alternative Android Application for [WireGuard](https://www.wireguard
|
||||||
|
|
||||||
## Inspiration
|
## Inspiration
|
||||||
|
|
||||||
The original inspiration for this app came from the inconvenience of having to manually turn VPN off and on while on different networks. This app was created to offer a free solution to this problem.
|
The original inspiration for this app came from the inconvenience of having to manually turn VPN off
|
||||||
|
and on while on different networks. This app was created to offer a free solution to this problem.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
@ -63,9 +67,8 @@ The original inspiration for this app came from the inconvenience of having to m
|
||||||
* Automatic service restart after reboot
|
* Automatic service restart after reboot
|
||||||
* Battery preservation measures
|
* Battery preservation measures
|
||||||
|
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
```
|
```
|
||||||
$ git clone https://github.com/zaneschepke/wgtunnel
|
$ git clone https://github.com/zaneschepke/wgtunnel
|
||||||
$ cd wgtunnel
|
$ cd wgtunnel
|
||||||
|
|
|
@ -19,9 +19,7 @@ android {
|
||||||
versionCode = Constants.VERSION_CODE
|
versionCode = Constants.VERSION_CODE
|
||||||
versionName = Constants.VERSION_NAME
|
versionName = Constants.VERSION_NAME
|
||||||
|
|
||||||
ksp {
|
ksp { arg("room.schemaLocation", "$projectDir/schemas") }
|
||||||
arg("room.schemaLocation", "$projectDir/schemas")
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room
|
getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room
|
||||||
|
@ -30,9 +28,7 @@ android {
|
||||||
resourceConfigurations.addAll(listOf("en"))
|
resourceConfigurations.addAll(listOf("en"))
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables { useSupportLibrary = true }
|
||||||
useSupportLibrary = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
@ -47,32 +43,41 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
storeFile = file(
|
// build
|
||||||
System.getenv().getOrDefault(
|
storeFile =
|
||||||
Constants.KEY_STORE_PATH_VAR,
|
file(
|
||||||
properties.getProperty(Constants.KEY_STORE_PATH_VAR)
|
System.getenv()
|
||||||
|
.getOrDefault(
|
||||||
|
Constants.KEY_STORE_PATH_VAR,
|
||||||
|
properties.getProperty(Constants.KEY_STORE_PATH_VAR),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
storePassword =
|
||||||
storePassword = System.getenv().getOrDefault(
|
System.getenv()
|
||||||
Constants.STORE_PASS_VAR,
|
.getOrDefault(
|
||||||
properties.getProperty(Constants.STORE_PASS_VAR)
|
Constants.STORE_PASS_VAR,
|
||||||
)
|
properties.getProperty(Constants.STORE_PASS_VAR),
|
||||||
keyAlias = System.getenv().getOrDefault(
|
)
|
||||||
Constants.KEY_ALIAS_VAR,
|
keyAlias =
|
||||||
properties.getProperty(Constants.KEY_ALIAS_VAR)
|
System.getenv()
|
||||||
)
|
.getOrDefault(
|
||||||
keyPassword = System.getenv().getOrDefault(
|
Constants.KEY_ALIAS_VAR,
|
||||||
Constants.KEY_PASS_VAR,
|
properties.getProperty(Constants.KEY_ALIAS_VAR),
|
||||||
properties.getProperty(Constants.KEY_PASS_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 {
|
||||||
|
@ -91,13 +96,11 @@ android {
|
||||||
isShrinkResources = true
|
isShrinkResources = true
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro",
|
||||||
)
|
)
|
||||||
signingConfig = signingConfigs.getByName(Constants.RELEASE)
|
signingConfig = signingConfigs.getByName(Constants.RELEASE)
|
||||||
}
|
}
|
||||||
debug {
|
debug { isDebuggable = true }
|
||||||
isDebuggable = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
flavorDimensions.add(Constants.TYPE)
|
flavorDimensions.add(Constants.TYPE)
|
||||||
productFlavors {
|
productFlavors {
|
||||||
|
@ -118,24 +121,17 @@ android {
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
isCoreLibraryDesugaringEnabled = true
|
isCoreLibraryDesugaringEnabled = true
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions { jvmTarget = Constants.JVM_TARGET }
|
||||||
jvmTarget = Constants.JVM_TARGET
|
|
||||||
}
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
}
|
}
|
||||||
composeOptions {
|
composeOptions { kotlinCompilerExtensionVersion = Constants.COMPOSE_COMPILER_EXTENSION_VERSION }
|
||||||
kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
|
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
|
||||||
}
|
|
||||||
packaging {
|
|
||||||
resources {
|
|
||||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val generalImplementation by configurations
|
val generalImplementation by configurations
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
|
|
|
@ -14,10 +14,11 @@ class MigrationTest {
|
||||||
private val dbName = "migration-test"
|
private val dbName = "migration-test"
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val helper: MigrationTestHelper = MigrationTestHelper(
|
val helper: MigrationTestHelper =
|
||||||
InstrumentationRegistry.getInstrumentation(),
|
MigrationTestHelper(
|
||||||
AppDatabase::class.java
|
InstrumentationRegistry.getInstrumentation(),
|
||||||
)
|
AppDatabase::class.java,
|
||||||
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
|
@ -27,34 +28,33 @@ class MigrationTest {
|
||||||
// You can't use DAO classes because they expect the latest schema.
|
// You can't use DAO classes because they expect the latest schema.
|
||||||
execSQL(
|
execSQL(
|
||||||
"INSERT INTO Settings (is_tunnel_enabled," +
|
"INSERT INTO Settings (is_tunnel_enabled," +
|
||||||
"is_tunnel_on_mobile_data_enabled," +
|
"is_tunnel_on_mobile_data_enabled," +
|
||||||
"trusted_network_ssids," +
|
"trusted_network_ssids," +
|
||||||
"default_tunnel," +
|
"default_tunnel," +
|
||||||
"is_always_on_vpn_enabled," +
|
"is_always_on_vpn_enabled," +
|
||||||
"is_tunnel_on_ethernet_enabled," +
|
"is_tunnel_on_ethernet_enabled," +
|
||||||
"is_shortcuts_enabled," +
|
"is_shortcuts_enabled," +
|
||||||
"is_battery_saver_enabled," +
|
"is_battery_saver_enabled," +
|
||||||
"is_tunnel_on_wifi_enabled," +
|
"is_tunnel_on_wifi_enabled," +
|
||||||
"is_kernel_enabled," +
|
"is_kernel_enabled," +
|
||||||
"is_restore_on_boot_enabled," +
|
"is_restore_on_boot_enabled," +
|
||||||
"is_multi_tunnel_enabled)" +
|
"is_multi_tunnel_enabled)" +
|
||||||
" VALUES " +
|
" VALUES " +
|
||||||
"('false'," +
|
"('false'," +
|
||||||
"'false'," +
|
"'false'," +
|
||||||
"'[trustedSSID1,trustedSSID2]'," +
|
"'[trustedSSID1,trustedSSID2]'," +
|
||||||
"'defaultTunnel'," +
|
"'defaultTunnel'," +
|
||||||
"'false'," +
|
"'false'," +
|
||||||
"'false'," +
|
"'false'," +
|
||||||
"'false'," +
|
"'false'," +
|
||||||
"'false'," +
|
"'false'," +
|
||||||
"'false'," +
|
"'false'," +
|
||||||
"'false'," +
|
"'false'," +
|
||||||
"'false'," +
|
"'false'," +
|
||||||
"'false')"
|
"'false')",
|
||||||
)
|
)
|
||||||
execSQL(
|
execSQL(
|
||||||
"INSERT INTO TunnelConfig (name, wg_quick)" +
|
"INSERT INTO TunnelConfig (name, wg_quick)" + " VALUES ('hello', 'hello')",
|
||||||
" VALUES ('hello', 'hello')"
|
|
||||||
)
|
)
|
||||||
// Prepare for the next version.
|
// Prepare for the next version.
|
||||||
close()
|
close()
|
||||||
|
|
|
@ -1,56 +1,63 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
<uses-permission
|
||||||
|
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="32" />
|
android:maxSdkVersion="32" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="32"
|
android:maxSdkVersion="32"
|
||||||
tools:ignore="ScopedStorage" />
|
tools:ignore="ScopedStorage" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"
|
<uses-permission
|
||||||
|
android:name="android.permission.ACCESS_WIFI_STATE"
|
||||||
android:maxSdkVersion="30"
|
android:maxSdkVersion="30"
|
||||||
tools:ignore="LeanbackUsesWifi" />
|
tools:ignore="LeanbackUsesWifi" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.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-->
|
<!--foreground service exempt android 14-->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
|
<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" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
<!--start service on boot permission-->
|
<!--start service on boot permission-->
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
<!--android tv support-->
|
<!--android tv support-->
|
||||||
<uses-feature android:name="android.software.leanback"
|
<uses-feature
|
||||||
|
android:name="android.software.leanback"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
<uses-feature android:name="android.hardware.touchscreen"
|
<uses-feature
|
||||||
|
android:name="android.hardware.touchscreen"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.location.gps"
|
android:name="android.hardware.location.gps"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.screen.portrait"
|
android:name="android.hardware.screen.portrait"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
</intent>
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
|
||||||
android:enableOnBackInvokedCallback="true"
|
|
||||||
android:name=".WireGuardAutoTunnel"
|
android:name=".WireGuardAutoTunnel"
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:banner="@mipmap/ic_banner"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:banner="@mipmap/ic_banner"
|
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
|
@ -62,11 +69,14 @@
|
||||||
android:theme="@style/Theme.WireguardAutoTunnel">
|
android:theme="@style/Theme.WireguardAutoTunnel">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES"/>
|
|
||||||
|
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data android:name="android.app.shortcuts"
|
<meta-data
|
||||||
|
android:name="android.app.shortcuts"
|
||||||
android:resource="@xml/shortcuts" />
|
android:resource="@xml/shortcuts" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
|
@ -76,65 +86,74 @@
|
||||||
android:theme="@style/zxing_CaptureTheme"
|
android:theme="@style/zxing_CaptureTheme"
|
||||||
android:windowSoftInputMode="stateAlwaysHidden" />
|
android:windowSoftInputMode="stateAlwaysHidden" />
|
||||||
<activity
|
<activity
|
||||||
android:finishOnTaskLaunch="true"
|
android:name=".service.shortcut.ShortcutsActivity"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@android:style/Theme.NoDisplay"
|
android:finishOnTaskLaunch="true"
|
||||||
android:name=".service.shortcut.ShortcutsActivity"/>
|
android:theme="@android:style/Theme.NoDisplay" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".service.foreground.ForegroundService"
|
android:name=".service.foreground.ForegroundService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
|
android:exported="false"
|
||||||
android:foregroundServiceType="systemExempted|specialUse"
|
android:foregroundServiceType="systemExempted|specialUse"
|
||||||
tools:node="merge"
|
tools:node="merge" />
|
||||||
android:exported="false">
|
|
||||||
</service>
|
|
||||||
<service
|
<service
|
||||||
android:exported="true"
|
|
||||||
android:name=".service.tile.TunnelControlTile"
|
android:name=".service.tile.TunnelControlTile"
|
||||||
|
android:exported="true"
|
||||||
android:icon="@drawable/shield"
|
android:icon="@drawable/shield"
|
||||||
android:label="WG Tunnel"
|
android:label="WG Tunnel"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||||
<meta-data android:name="android.service.quicksettings.ACTIVE_TILE"
|
<meta-data
|
||||||
|
android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
<meta-data android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
<meta-data
|
||||||
|
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name=".service.foreground.WireGuardTunnelService"
|
android:name=".service.foreground.WireGuardTunnelService"
|
||||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:persistent="true"
|
android:exported="false"
|
||||||
android:foregroundServiceType="systemExempted|specialUse"
|
android:foregroundServiceType="systemExempted|specialUse"
|
||||||
tools:node="merge"
|
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||||
android:exported="false">
|
android:persistent="true"
|
||||||
|
tools:node="merge">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.net.VpnService"/>
|
<action android:name="android.net.VpnService" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
<meta-data
|
||||||
|
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name=".service.foreground.WireGuardConnectivityWatcherService"
|
android:name=".service.foreground.WireGuardConnectivityWatcherService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:stopWithTask="false"
|
android:exported="false"
|
||||||
android:persistent="true"
|
|
||||||
android:foregroundServiceType="systemExempted|specialUse"
|
android:foregroundServiceType="systemExempted|specialUse"
|
||||||
tools:node="merge"
|
android:persistent="true"
|
||||||
android:exported="false">
|
android:stopWithTask="false"
|
||||||
</service>
|
tools:node="merge" />
|
||||||
<receiver android:enabled="true" android:name=".receiver.BootReceiver"
|
|
||||||
|
<receiver
|
||||||
|
android:name=".receiver.BootReceiver"
|
||||||
|
android:enabled="true"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
|
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
|
||||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver android:exported="false" android:name=".receiver.NotificationActionReceiver"/>
|
<receiver
|
||||||
|
android:name=".receiver.NotificationActionReceiver"
|
||||||
|
android:exported="false" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
|
@ -4,61 +4,31 @@ import android.app.Application
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.IOException
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class WireGuardAutoTunnel : Application() {
|
class WireGuardAutoTunnel : Application() {
|
||||||
@Inject
|
|
||||||
lateinit var settingsRepository: SettingsRepository
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var dataStoreManager: DataStoreManager
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
instance = this
|
instance = this
|
||||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
if (BuildConfig.DEBUG) 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initSettings() {
|
|
||||||
with(ProcessLifecycleOwner.get()) {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
if (settingsRepository.getAll().isEmpty()) {
|
|
||||||
settingsRepository.save(Settings())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
lateinit var instance: WireGuardAutoTunnel private set
|
lateinit var instance: WireGuardAutoTunnel
|
||||||
|
private set
|
||||||
|
|
||||||
fun isRunningOnAndroidTv(): Boolean {
|
fun isRunningOnAndroidTv(): Boolean {
|
||||||
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestTileServiceStateUpdate() {
|
fun requestTileServiceStateUpdate() {
|
||||||
TileService.requestListeningState(instance, ComponentName(instance, TunnelControlTile::class.java))
|
TileService.requestListeningState(
|
||||||
|
instance,
|
||||||
|
ComponentName(instance, TunnelControlTile::class.java),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,16 +10,20 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
@Database(
|
@Database(
|
||||||
entities = [Settings::class, TunnelConfig::class],
|
entities = [Settings::class, TunnelConfig::class],
|
||||||
version = 5,
|
version = 5,
|
||||||
autoMigrations = [
|
autoMigrations =
|
||||||
AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3), AutoMigration(
|
[
|
||||||
from = 3,
|
AutoMigration(from = 1, to = 2),
|
||||||
to = 4
|
AutoMigration(from = 2, to = 3),
|
||||||
),AutoMigration(
|
AutoMigration(
|
||||||
from = 4,
|
from = 3,
|
||||||
to = 5
|
to = 4,
|
||||||
)
|
),
|
||||||
],
|
AutoMigration(
|
||||||
exportSchema = true
|
from = 4,
|
||||||
|
to = 5,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
exportSchema = true,
|
||||||
)
|
)
|
||||||
@TypeConverters(DatabaseListConverters::class)
|
@TypeConverters(DatabaseListConverters::class)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
|
|
@ -10,27 +10,19 @@ import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface SettingsDao {
|
interface SettingsDao {
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: Settings)
|
||||||
suspend fun save(t: Settings)
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<Settings>)
|
||||||
suspend fun saveAll(t: List<Settings>)
|
|
||||||
|
|
||||||
@Query("SELECT * FROM settings WHERE id=:id")
|
@Query("SELECT * FROM settings WHERE id=:id") suspend fun getById(id: Long): Settings?
|
||||||
suspend fun getById(id: Long): Settings?
|
|
||||||
|
|
||||||
@Query("SELECT * FROM settings")
|
@Query("SELECT * FROM settings") suspend fun getAll(): List<Settings>
|
||||||
suspend fun getAll(): List<Settings>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM settings LIMIT 1")
|
@Query("SELECT * FROM settings LIMIT 1") fun getSettingsFlow(): Flow<Settings>
|
||||||
fun getSettingsFlow(): Flow<Settings>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM settings")
|
@Query("SELECT * FROM settings") fun getAllFlow(): Flow<MutableList<Settings>>
|
||||||
fun getAllFlow(): Flow<MutableList<Settings>>
|
|
||||||
|
|
||||||
@Delete
|
@Delete suspend fun delete(t: Settings)
|
||||||
suspend fun delete(t: Settings)
|
|
||||||
|
|
||||||
@Query("SELECT COUNT('id') FROM settings")
|
@Query("SELECT COUNT('id') FROM settings") suspend fun count(): Long
|
||||||
suspend fun count(): Long
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,24 +10,17 @@ 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)
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<TunnelConfig>)
|
||||||
suspend fun saveAll(t: List<TunnelConfig>)
|
|
||||||
|
|
||||||
@Query("SELECT * FROM TunnelConfig WHERE id=:id")
|
@Query("SELECT * FROM TunnelConfig WHERE id=:id") suspend fun getById(id: Long): TunnelConfig?
|
||||||
suspend fun getById(id: Long): TunnelConfig?
|
|
||||||
|
|
||||||
@Query("SELECT * FROM TunnelConfig")
|
@Query("SELECT * FROM TunnelConfig") suspend fun getAll(): List<TunnelConfig>
|
||||||
suspend fun getAll(): List<TunnelConfig>
|
|
||||||
|
|
||||||
@Delete
|
@Delete suspend fun delete(t: TunnelConfig)
|
||||||
suspend fun delete(t: TunnelConfig)
|
|
||||||
|
|
||||||
@Query("SELECT COUNT('id') FROM TunnelConfig")
|
@Query("SELECT COUNT('id') FROM TunnelConfig") suspend fun count(): Long
|
||||||
suspend fun count(): Long
|
|
||||||
|
|
||||||
@Query("SELECT * FROM tunnelconfig")
|
@Query("SELECT * FROM tunnelconfig") fun getAllFlow(): Flow<MutableList<TunnelConfig>>
|
||||||
fun getAllFlow(): Flow<MutableList<TunnelConfig>>
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.data.datastore
|
package com.zaneschepke.wireguardautotunnel.data.datastore
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
|
@ -11,29 +12,27 @@ import kotlinx.coroutines.flow.map
|
||||||
class DataStoreManager(private val context: Context) {
|
class DataStoreManager(private val context: Context) {
|
||||||
companion object {
|
companion object {
|
||||||
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
|
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
|
||||||
|
val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
|
||||||
}
|
}
|
||||||
|
|
||||||
// preferences
|
// preferences
|
||||||
private val preferencesKey = "preferences"
|
private val preferencesKey = "preferences"
|
||||||
private val Context.dataStore by preferencesDataStore(
|
private val Context.dataStore by
|
||||||
name = preferencesKey
|
preferencesDataStore(
|
||||||
)
|
name = preferencesKey,
|
||||||
|
)
|
||||||
|
|
||||||
suspend fun init() {
|
suspend fun init() {
|
||||||
context.dataStore.data.first()
|
context.dataStore.data.first()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) =
|
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) =
|
||||||
context.dataStore.edit {
|
context.dataStore.edit { it[key] = value }
|
||||||
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]
|
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] }
|
||||||
|
|
||||||
val locationDisclosureFlow: Flow<Boolean?> = context.dataStore.data.map {
|
suspend fun <T> getFromStore(key: Preferences.Key<T>) =
|
||||||
it[LOCATION_DISCLOSURE_SHOWN]
|
context.dataStore.data.first { it.contains(key) }[key]
|
||||||
}
|
|
||||||
|
val preferencesFlow: Flow<Preferences?> = context.dataStore.data
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,39 +8,49 @@ import androidx.room.PrimaryKey
|
||||||
data class Settings(
|
data class Settings(
|
||||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||||
@ColumnInfo(name = "is_tunnel_enabled") var isAutoTunnelEnabled: Boolean = false,
|
@ColumnInfo(name = "is_tunnel_enabled") var isAutoTunnelEnabled: Boolean = false,
|
||||||
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled") var isTunnelOnMobileDataEnabled: Boolean = false,
|
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled")
|
||||||
@ColumnInfo(name = "trusted_network_ssids") var trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
|
var isTunnelOnMobileDataEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(name = "trusted_network_ssids")
|
||||||
|
var trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
|
||||||
@ColumnInfo(name = "default_tunnel") var defaultTunnel: String? = null,
|
@ColumnInfo(name = "default_tunnel") var defaultTunnel: String? = null,
|
||||||
@ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled: Boolean = false,
|
@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_tunnel_on_ethernet_enabled")
|
||||||
|
var isTunnelOnEthernetEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_shortcuts_enabled",
|
name = "is_shortcuts_enabled",
|
||||||
defaultValue = "false"
|
defaultValue = "false",
|
||||||
) var isShortcutsEnabled: Boolean = false,
|
)
|
||||||
|
var isShortcutsEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_battery_saver_enabled",
|
name = "is_battery_saver_enabled",
|
||||||
defaultValue = "false"
|
defaultValue = "false",
|
||||||
) var isBatterySaverEnabled: Boolean = false,
|
)
|
||||||
|
var isBatterySaverEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_tunnel_on_wifi_enabled",
|
name = "is_tunnel_on_wifi_enabled",
|
||||||
defaultValue = "false"
|
defaultValue = "false",
|
||||||
) var isTunnelOnWifiEnabled: Boolean = false,
|
)
|
||||||
|
var isTunnelOnWifiEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_kernel_enabled",
|
name = "is_kernel_enabled",
|
||||||
defaultValue = "false"
|
defaultValue = "false",
|
||||||
) var isKernelEnabled: Boolean = false,
|
)
|
||||||
|
var isKernelEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_restore_on_boot_enabled",
|
name = "is_restore_on_boot_enabled",
|
||||||
defaultValue = "false"
|
defaultValue = "false",
|
||||||
) var isRestoreOnBootEnabled: Boolean = false,
|
)
|
||||||
|
var isRestoreOnBootEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_multi_tunnel_enabled",
|
name = "is_multi_tunnel_enabled",
|
||||||
defaultValue = "false"
|
defaultValue = "false",
|
||||||
) var isMultiTunnelEnabled: Boolean = false,
|
)
|
||||||
|
var isMultiTunnelEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_auto_tunnel_paused",
|
name = "is_auto_tunnel_paused",
|
||||||
defaultValue = "false"
|
defaultValue = "false",
|
||||||
) var isAutoTunnelPaused: Boolean = false,
|
)
|
||||||
|
var isAutoTunnelPaused: Boolean = false,
|
||||||
) {
|
) {
|
||||||
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean {
|
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean {
|
||||||
return if (defaultTunnel != null) {
|
return if (defaultTunnel != null) {
|
||||||
|
|
|
@ -4,9 +4,11 @@ import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface SettingsRepository {
|
interface SettingsRepository {
|
||||||
suspend fun save(settings : Settings)
|
suspend fun save(settings: Settings)
|
||||||
fun getSettingsFlow() : Flow<Settings>
|
|
||||||
|
|
||||||
suspend fun getSettings() : Settings
|
fun getSettingsFlow(): Flow<Settings>
|
||||||
suspend fun getAll() : List<Settings>
|
|
||||||
}
|
suspend fun getSettings(): Settings
|
||||||
|
|
||||||
|
suspend fun getAll(): List<Settings>
|
||||||
|
}
|
||||||
|
|
|
@ -21,4 +21,4 @@ class SettingsRepositoryImpl(private val settingsDoa: SettingsDao) : SettingsRep
|
||||||
override suspend fun getAll(): List<Settings> {
|
override suspend fun getAll(): List<Settings> {
|
||||||
return settingsDoa.getAll()
|
return settingsDoa.getAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,13 @@ import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface TunnelConfigRepository {
|
interface TunnelConfigRepository {
|
||||||
|
|
||||||
fun getTunnelConfigsFlow() : Flow<TunnelConfigs>
|
fun getTunnelConfigsFlow(): Flow<TunnelConfigs>
|
||||||
suspend fun getAll() : TunnelConfigs
|
|
||||||
|
suspend fun getAll(): TunnelConfigs
|
||||||
|
|
||||||
suspend fun save(tunnelConfig: TunnelConfig)
|
suspend fun save(tunnelConfig: TunnelConfig)
|
||||||
|
|
||||||
suspend fun delete(tunnelConfig: TunnelConfig)
|
suspend fun delete(tunnelConfig: TunnelConfig)
|
||||||
suspend fun count() : Int
|
|
||||||
}
|
suspend fun count(): Int
|
||||||
|
}
|
||||||
|
|
|
@ -5,7 +5,8 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
class TunnelConfigRepositoryImpl(private val tunnelConfigDao: TunnelConfigDao) : TunnelConfigRepository {
|
class TunnelConfigRepositoryImpl(private val tunnelConfigDao: TunnelConfigDao) :
|
||||||
|
TunnelConfigRepository {
|
||||||
override fun getTunnelConfigsFlow(): Flow<TunnelConfigs> {
|
override fun getTunnelConfigsFlow(): Flow<TunnelConfigs> {
|
||||||
return tunnelConfigDao.getAllFlow()
|
return tunnelConfigDao.getAllFlow()
|
||||||
}
|
}
|
||||||
|
@ -25,4 +26,4 @@ class TunnelConfigRepositoryImpl(private val tunnelConfigDao: TunnelConfigDao) :
|
||||||
override suspend fun count(): Int {
|
override suspend fun count(): Int {
|
||||||
return tunnelConfigDao.count().toInt()
|
return tunnelConfigDao.count().toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,14 +16,12 @@ 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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,4 @@ package com.zaneschepke.wireguardautotunnel.module
|
||||||
|
|
||||||
import javax.inject.Qualifier
|
import javax.inject.Qualifier
|
||||||
|
|
||||||
@Qualifier
|
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Kernel
|
||||||
@Retention(AnnotationRetention.BINARY)
|
|
||||||
annotation class Kernel
|
|
||||||
|
|
|
@ -17,7 +17,9 @@ import dagger.hilt.android.scopes.ServiceScoped
|
||||||
abstract class ServiceModule {
|
abstract class ServiceModule {
|
||||||
@Binds
|
@Binds
|
||||||
@ServiceScoped
|
@ServiceScoped
|
||||||
abstract fun provideNotificationService(wireGuardNotification: WireGuardNotification): NotificationService
|
abstract fun provideNotificationService(
|
||||||
|
wireGuardNotification: WireGuardNotification
|
||||||
|
): NotificationService
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@ServiceScoped
|
@ServiceScoped
|
||||||
|
@ -25,9 +27,13 @@ abstract class ServiceModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@ServiceScoped
|
@ServiceScoped
|
||||||
abstract fun provideMobileDataService(mobileDataService: MobileDataService): NetworkService<MobileDataService>
|
abstract fun provideMobileDataService(
|
||||||
|
mobileDataService: MobileDataService
|
||||||
|
): NetworkService<MobileDataService>
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@ServiceScoped
|
@ServiceScoped
|
||||||
abstract fun provideEthernetService(ethernetService: EthernetService): NetworkService<EthernetService>
|
abstract fun provideEthernetService(
|
||||||
|
ethernetService: EthernetService
|
||||||
|
): NetworkService<EthernetService>
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,28 +21,21 @@ import javax.inject.Singleton
|
||||||
class TunnelModule {
|
class TunnelModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideRootShell(
|
fun provideRootShell(@ApplicationContext context: Context): RootShell {
|
||||||
@ApplicationContext context: Context
|
|
||||||
): RootShell {
|
|
||||||
return RootShell(context)
|
return RootShell(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
@Userspace
|
@Userspace
|
||||||
fun provideUserspaceBackend(
|
fun provideUserspaceBackend(@ApplicationContext context: Context): Backend {
|
||||||
@ApplicationContext context: Context
|
|
||||||
): Backend {
|
|
||||||
return GoBackend(context)
|
return GoBackend(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
@Kernel
|
@Kernel
|
||||||
fun provideKernelBackend(
|
fun provideKernelBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend {
|
||||||
@ApplicationContext context: Context,
|
|
||||||
rootShell: RootShell
|
|
||||||
): Backend {
|
|
||||||
return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell))
|
return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,7 +44,7 @@ class TunnelModule {
|
||||||
fun provideVpnService(
|
fun provideVpnService(
|
||||||
@Userspace userspaceBackend: Backend,
|
@Userspace userspaceBackend: Backend,
|
||||||
@Kernel kernelBackend: Backend,
|
@Kernel kernelBackend: Backend,
|
||||||
settingsRepository : SettingsRepository
|
settingsRepository: SettingsRepository
|
||||||
): VpnService {
|
): VpnService {
|
||||||
return WireGuardTunnel(userspaceBackend, kernelBackend, settingsRepository)
|
return WireGuardTunnel(userspaceBackend, kernelBackend, settingsRepository)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,4 @@ package com.zaneschepke.wireguardautotunnel.module
|
||||||
|
|
||||||
import javax.inject.Qualifier
|
import javax.inject.Qualifier
|
||||||
|
|
||||||
@Qualifier
|
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Userspace
|
||||||
@Retention(AnnotationRetention.BINARY)
|
|
||||||
annotation class Userspace
|
|
||||||
|
|
|
@ -9,17 +9,15 @@ import com.zaneschepke.wireguardautotunnel.util.goAsync
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class BootReceiver : BroadcastReceiver() {
|
class BootReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
lateinit var settingsRepository: SettingsRepository
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) = goAsync {
|
override fun onReceive(context: Context?, intent: Intent?) = goAsync {
|
||||||
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
|
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
|
||||||
if(settingsRepository.getSettings().isAutoTunnelEnabled) {
|
if (settingsRepository.getSettings().isAutoTunnelEnabled) {
|
||||||
ServiceManager.startWatcherServiceForeground(context!!)
|
ServiceManager.startWatcherServiceForeground(context!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,13 +14,9 @@ import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class NotificationActionReceiver : BroadcastReceiver() {
|
class NotificationActionReceiver : BroadcastReceiver() {
|
||||||
@Inject
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
lateinit var settingsRepository: SettingsRepository
|
|
||||||
|
|
||||||
override fun onReceive(
|
override fun onReceive(context: Context, intent: Intent?) = goAsync {
|
||||||
context: Context,
|
|
||||||
intent: Intent?
|
|
||||||
) = goAsync {
|
|
||||||
try {
|
try {
|
||||||
val settings = settingsRepository.getSettings()
|
val settings = settingsRepository.getSettings()
|
||||||
if (settings.defaultTunnel != null) {
|
if (settings.defaultTunnel != null) {
|
||||||
|
|
|
@ -15,18 +15,14 @@ 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) {
|
||||||
val action = intent.action
|
val action = intent.action
|
||||||
Timber.d("using an intent with action $action")
|
|
||||||
when (action) {
|
when (action) {
|
||||||
Action.START.name, Action.START_FOREGROUND.name -> startService(intent.extras)
|
Action.START.name,
|
||||||
|
Action.START_FOREGROUND.name -> startService(intent.extras)
|
||||||
Action.STOP.name -> stopService(intent.extras)
|
Action.STOP.name -> stopService(intent.extras)
|
||||||
"android.net.VpnService" -> {
|
"android.net.VpnService" -> {
|
||||||
Timber.d("Always-on VPN starting service")
|
Timber.d("Always-on VPN starting service")
|
||||||
|
@ -36,7 +32,7 @@ open class ForegroundService : LifecycleService() {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Timber.d(
|
Timber.d(
|
||||||
"with a null intent. It has been probably restarted by the system."
|
"with a null intent. It has been probably restarted by the system.",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// by returning this we make sure the service is restarted if the system kills the service
|
// by returning this we make sure the service is restarted if the system kills the service
|
||||||
|
|
|
@ -1,28 +1,27 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||||
|
|
||||||
import android.app.ActivityManager
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Context.ACTIVITY_SERVICE
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
object ServiceManager {
|
object ServiceManager {
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private // Deprecated for third party Services.
|
|
||||||
fun <T> Context.isServiceRunning(service: Class<T>) =
|
|
||||||
(getSystemService(ACTIVITY_SERVICE) as ActivityManager)
|
|
||||||
.getRunningServices(Integer.MAX_VALUE)
|
|
||||||
.any { it.service.className == service.name }
|
|
||||||
|
|
||||||
fun <T : Service> getServiceState(
|
// private
|
||||||
context: Context,
|
// fun <T> Context.isServiceRunning(service: Class<T>) =
|
||||||
cls: Class<T>
|
// (getSystemService(ACTIVITY_SERVICE) as ActivityManager)
|
||||||
): ServiceState {
|
// .runningAppProcesses.any {
|
||||||
val isServiceRunning = context.isServiceRunning(cls)
|
// it.processName == service.name
|
||||||
return if (isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED
|
// }
|
||||||
}
|
//
|
||||||
|
// fun <T : Service> getServiceState(
|
||||||
|
// context: Context,
|
||||||
|
// cls: Class<T>
|
||||||
|
// ): ServiceState {
|
||||||
|
// val isServiceRunning = context.isServiceRunning(cls)
|
||||||
|
// return if (isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED
|
||||||
|
// }
|
||||||
|
|
||||||
private fun <T : Service> actionOnService(
|
private fun <T : Service> actionOnService(
|
||||||
action: Action,
|
action: Action,
|
||||||
|
@ -30,14 +29,10 @@ object ServiceManager {
|
||||||
cls: Class<T>,
|
cls: Class<T>,
|
||||||
extras: Map<String, String>? = null
|
extras: Map<String, String>? = null
|
||||||
) {
|
) {
|
||||||
if (getServiceState(context, cls) == ServiceState.STOPPED && action == Action.STOP) 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 {
|
||||||
|
@ -45,11 +40,9 @@ object ServiceManager {
|
||||||
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) {
|
||||||
|
@ -57,35 +50,30 @@ object ServiceManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
Timber.d("Stopping vpn service action")
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.STOP,
|
Action.STOP,
|
||||||
context,
|
context,
|
||||||
WireGuardTunnelService::class.java
|
WireGuardTunnelService::class.java,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,17 +83,15 @@ object ServiceManager {
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.START_FOREGROUND,
|
Action.START_FOREGROUND,
|
||||||
context,
|
context,
|
||||||
WireGuardConnectivityWatcherService::class.java
|
WireGuardConnectivityWatcherService::class.java,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startWatcherService(
|
fun startWatcherService(context: Context) {
|
||||||
context: Context
|
|
||||||
) {
|
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.START,
|
Action.START,
|
||||||
context,
|
context,
|
||||||
WireGuardConnectivityWatcherService::class.java
|
WireGuardConnectivityWatcherService::class.java,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,7 +99,7 @@ object ServiceManager {
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.STOP,
|
Action.STOP,
|
||||||
context,
|
context,
|
||||||
WireGuardConnectivityWatcherService::class.java
|
WireGuardConnectivityWatcherService::class.java,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,334 +34,361 @@ import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class WireGuardConnectivityWatcherService : ForegroundService() {
|
class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
private val foregroundId = 122
|
private val foregroundId = 122
|
||||||
|
|
||||||
@Inject lateinit var wifiService: NetworkService<WifiService>
|
@Inject lateinit var wifiService: NetworkService<WifiService>
|
||||||
|
|
||||||
@Inject lateinit var mobileDataService: NetworkService<MobileDataService>
|
@Inject lateinit var mobileDataService: NetworkService<MobileDataService>
|
||||||
|
|
||||||
@Inject lateinit var ethernetService: NetworkService<EthernetService>
|
@Inject lateinit var ethernetService: NetworkService<EthernetService>
|
||||||
|
|
||||||
@Inject lateinit var settingsRepository: SettingsRepository
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
|
|
||||||
@Inject lateinit var notificationService: NotificationService
|
@Inject lateinit var notificationService: NotificationService
|
||||||
|
|
||||||
@Inject lateinit var vpnService: VpnService
|
@Inject lateinit var vpnService: VpnService
|
||||||
|
|
||||||
private val networkEventsFlow = MutableStateFlow(WatcherState())
|
private val networkEventsFlow = MutableStateFlow(WatcherState())
|
||||||
data class WatcherState(
|
|
||||||
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
|
data class WatcherState(
|
||||||
|
val isWifiConnected: Boolean = false,
|
||||||
|
val isVpnConnected: Boolean = false,
|
||||||
|
val isEthernetConnected: Boolean = false,
|
||||||
|
val isMobileDataConnected: Boolean = false,
|
||||||
|
val currentNetworkSSID: String = "",
|
||||||
|
val settings: Settings = Settings()
|
||||||
|
)
|
||||||
|
|
||||||
private var wakeLock: PowerManager.WakeLock? = null
|
private lateinit var watcherJob: Job
|
||||||
private val tag = this.javaClass.name
|
|
||||||
|
|
||||||
override fun onCreate() {
|
private var wakeLock: PowerManager.WakeLock? = null
|
||||||
super.onCreate()
|
private val tag = this.javaClass.name
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
|
||||||
try {
|
override fun onCreate() {
|
||||||
if(settingsRepository.getSettings().isAutoTunnelPaused) {
|
super.onCreate()
|
||||||
launchWatcherPausedNotification()
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
} else launchWatcherNotification()
|
try {
|
||||||
} catch (e: Exception) {
|
if (settingsRepository.getSettings().isAutoTunnelPaused) {
|
||||||
Timber.e("Failed to start watcher service, not enough permissions")
|
launchWatcherPausedNotification()
|
||||||
}
|
} else launchWatcherNotification()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
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 {
|
try {
|
||||||
// we need this lock so our service gets not affected by Doze Mode
|
// we need this lock so our service gets not affected by Doze Mode
|
||||||
lifecycleScope.launch { initWakeLock() }
|
lifecycleScope.launch { initWakeLock() }
|
||||||
cancelWatcherJob()
|
cancelWatcherJob()
|
||||||
startWatcherJob()
|
startWatcherJob()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e("Failed to launch watcher service, no permissions")
|
Timber.e("Failed to launch watcher service, no permissions")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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 =
|
description: String = getString(R.string.watcher_notification_text_active)
|
||||||
notificationService.createNotification(
|
) {
|
||||||
channelId = getString(R.string.watcher_channel_id),
|
val notification =
|
||||||
channelName = getString(R.string.watcher_channel_name),
|
notificationService.createNotification(
|
||||||
title = getString(R.string.auto_tunnel_title),
|
channelId = getString(R.string.watcher_channel_id),
|
||||||
description = description)
|
channelName = getString(R.string.watcher_channel_name),
|
||||||
ServiceCompat.startForeground(
|
title = getString(R.string.auto_tunnel_title),
|
||||||
this, foregroundId, notification, Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID)
|
description = description,
|
||||||
}
|
)
|
||||||
|
ServiceCompat.startForeground(
|
||||||
|
this,
|
||||||
|
foregroundId,
|
||||||
|
notification,
|
||||||
|
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun launchWatcherPausedNotification() {
|
private fun launchWatcherPausedNotification() {
|
||||||
launchWatcherNotification(getString(R.string.watcher_notification_text_paused))
|
launchWatcherNotification(getString(R.string.watcher_notification_text_paused))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO could this be restarting service in a bad state?
|
// 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 =
|
val restartServicePendingIntent: PendingIntent =
|
||||||
PendingIntent.getService(
|
PendingIntent.getService(
|
||||||
this,
|
this,
|
||||||
1,
|
1,
|
||||||
restartServiceIntent,
|
restartServiceIntent,
|
||||||
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE)
|
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
|
||||||
applicationContext.getSystemService(Context.ALARM_SERVICE)
|
)
|
||||||
val alarmService: AlarmManager =
|
applicationContext.getSystemService(Context.ALARM_SERVICE)
|
||||||
applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
val alarmService: AlarmManager =
|
||||||
alarmService.set(
|
applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||||
AlarmManager.ELAPSED_REALTIME,
|
alarmService.set(
|
||||||
SystemClock.elapsedRealtime() + 1000,
|
AlarmManager.ELAPSED_REALTIME,
|
||||||
restartServicePendingIntent)
|
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 =
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
withContext(lifecycleScope.coroutineContext) {
|
||||||
val setting = settingsRepository.getSettings()
|
settingsRepository.getSettings().isBatterySaverEnabled
|
||||||
launch {
|
|
||||||
Timber.d("Starting wifi watcher")
|
|
||||||
watchForWifiConnectivityChanges()
|
|
||||||
}
|
|
||||||
if (setting.isTunnelOnMobileDataEnabled) {
|
|
||||||
launch {
|
|
||||||
Timber.d("Starting mobile data watcher")
|
|
||||||
watchForMobileDataConnectivityChanges()
|
|
||||||
}
|
}
|
||||||
}
|
wakeLock =
|
||||||
if (setting.isTunnelOnEthernetEnabled) {
|
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||||
launch {
|
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
||||||
Timber.d("Starting ethernet data watcher")
|
if (isBatterySaverOn) {
|
||||||
watchForEthernetConnectivityChanges()
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
launch {
|
|
||||||
Timber.d("Starting vpn state watcher")
|
|
||||||
watchForVpnConnectivityChanges()
|
|
||||||
}
|
|
||||||
launch {
|
|
||||||
Timber.d("Starting settings watcher")
|
|
||||||
watchForSettingsChanges()
|
|
||||||
}
|
|
||||||
launch {
|
|
||||||
Timber.d("Starting management watcher")
|
|
||||||
manageVpn()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 fun cancelWatcherJob() {
|
||||||
|
if (this::watcherJob.isInitialized) {
|
||||||
|
watcherJob.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startWatcherJob() {
|
||||||
|
watcherJob =
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val setting = settingsRepository.getSettings()
|
||||||
|
launch {
|
||||||
|
Timber.d("Starting wifi watcher")
|
||||||
|
watchForWifiConnectivityChanges()
|
||||||
|
}
|
||||||
|
if (setting.isTunnelOnMobileDataEnabled) {
|
||||||
|
launch {
|
||||||
|
Timber.d("Starting mobile data watcher")
|
||||||
|
watchForMobileDataConnectivityChanges()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (setting.isTunnelOnEthernetEnabled) {
|
||||||
|
launch {
|
||||||
|
Timber.d("Starting ethernet data watcher")
|
||||||
|
watchForEthernetConnectivityChanges()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
Timber.d("Starting vpn state watcher")
|
||||||
|
watchForVpnConnectivityChanges()
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
Timber.d("Starting settings watcher")
|
||||||
|
watchForSettingsChanges()
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
Timber.d("Starting management watcher")
|
||||||
|
manageVpn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
private suspend fun watchForSettingsChanges() {
|
||||||
settingsRepository.getSettingsFlow().collect {
|
settingsRepository.getSettingsFlow().collect {
|
||||||
if(networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
|
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
|
||||||
when(it.isAutoTunnelPaused) {
|
when (it.isAutoTunnelPaused) {
|
||||||
true -> launchWatcherPausedNotification()
|
true -> launchWatcherPausedNotification()
|
||||||
false -> launchWatcherNotification()
|
false -> launchWatcherNotification()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
networkEventsFlow.value =
|
||||||
settings = it
|
networkEventsFlow.value.copy(
|
||||||
)
|
settings = it,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun watchForVpnConnectivityChanges() {
|
private suspend fun watchForVpnConnectivityChanges() {
|
||||||
vpnService.vpnState.collect {
|
vpnService.vpnState.collect {
|
||||||
when(it.status) {
|
when (it.status) {
|
||||||
Tunnel.State.DOWN -> networkEventsFlow.value = networkEventsFlow.value.copy(
|
Tunnel.State.DOWN ->
|
||||||
isVpnConnected = false
|
networkEventsFlow.value =
|
||||||
)
|
networkEventsFlow.value.copy(
|
||||||
Tunnel.State.UP -> networkEventsFlow.value = networkEventsFlow.value.copy(
|
isVpnConnected = false,
|
||||||
isVpnConnected = true
|
)
|
||||||
)
|
Tunnel.State.UP ->
|
||||||
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isVpnConnected = true,
|
||||||
|
)
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun watchForEthernetConnectivityChanges() {
|
private suspend fun watchForEthernetConnectivityChanges() {
|
||||||
ethernetService.networkStatus.collect {
|
ethernetService.networkStatus.collect {
|
||||||
when (it) {
|
when (it) {
|
||||||
is NetworkStatus.Available -> {
|
is NetworkStatus.Available -> {
|
||||||
Timber.d("Gained Ethernet connection")
|
Timber.d("Gained Ethernet connection")
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
networkEventsFlow.value =
|
||||||
isEthernetConnected = true
|
networkEventsFlow.value.copy(
|
||||||
)
|
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() {
|
|
||||||
wifiService.networkStatus.collect {
|
|
||||||
when (it) {
|
|
||||||
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
|
|
||||||
private suspend fun manageVpn() {
|
|
||||||
networkEventsFlow.collectLatest {
|
|
||||||
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 &&
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
it.settings.isTunnelOnMobileDataEnabled &&
|
Timber.d("Ethernet capabilities changed")
|
||||||
!it.isWifiConnected &&
|
networkEventsFlow.value =
|
||||||
it.isMobileDataConnected &&
|
networkEventsFlow.value.copy(
|
||||||
!it.isVpnConnected) -> {
|
isEthernetConnected = true,
|
||||||
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
)
|
||||||
Timber.i("Condition 2 met")
|
|
||||||
}
|
}
|
||||||
(!it.isEthernetConnected &&
|
is NetworkStatus.Unavailable -> {
|
||||||
!it.settings.isTunnelOnMobileDataEnabled &&
|
networkEventsFlow.value =
|
||||||
!it.isWifiConnected &&
|
networkEventsFlow.value.copy(
|
||||||
it.isVpnConnected) -> {
|
isEthernetConnected = false,
|
||||||
ServiceManager.stopVpnService(this)
|
)
|
||||||
Timber.i("Condition 3 met")
|
Timber.d("Lost Ethernet connection")
|
||||||
}
|
}
|
||||||
(!it.isEthernetConnected &&
|
}
|
||||||
it.isWifiConnected &&
|
}
|
||||||
!it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID) &&
|
}
|
||||||
it.settings.isTunnelOnWifiEnabled &&
|
|
||||||
(!it.isVpnConnected)) -> {
|
private suspend fun watchForWifiConnectivityChanges() {
|
||||||
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
wifiService.networkStatus.collect {
|
||||||
Timber.i("Condition 4 met")
|
when (it) {
|
||||||
}
|
is NetworkStatus.Available -> {
|
||||||
(!it.isEthernetConnected &&
|
Timber.d("Gained Wi-Fi connection")
|
||||||
(it.isWifiConnected && it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) &&
|
networkEventsFlow.value =
|
||||||
(it.isVpnConnected)) -> {
|
networkEventsFlow.value.copy(
|
||||||
ServiceManager.stopVpnService(this)
|
isWifiConnected = true,
|
||||||
Timber.i("Condition 5 met")
|
)
|
||||||
}
|
}
|
||||||
(!it.isEthernetConnected &&
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
(it.isWifiConnected &&
|
Timber.d("Wifi capabilities changed")
|
||||||
!it.settings.isTunnelOnWifiEnabled &&
|
networkEventsFlow.value =
|
||||||
(it.isVpnConnected))) -> {
|
networkEventsFlow.value.copy(
|
||||||
ServiceManager.stopVpnService(this)
|
isWifiConnected = true,
|
||||||
Timber.i("Condition 6 met")
|
)
|
||||||
}
|
val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: ""
|
||||||
(!it.isEthernetConnected &&
|
Timber.d("Detected SSID: $ssid")
|
||||||
!it.isWifiConnected &&
|
networkEventsFlow.value =
|
||||||
!it.isMobileDataConnected &&
|
networkEventsFlow.value.copy(
|
||||||
(it.isVpnConnected)) -> {
|
currentNetworkSSID = ssid,
|
||||||
ServiceManager.stopVpnService(this)
|
)
|
||||||
Timber.i("Condition 7 met")
|
}
|
||||||
}
|
is NetworkStatus.Unavailable -> {
|
||||||
else -> {
|
networkEventsFlow.value =
|
||||||
Timber.i("No condition met")
|
networkEventsFlow.value.copy(
|
||||||
|
isWifiConnected = false,
|
||||||
|
)
|
||||||
|
Timber.d("Lost Wi-Fi connection")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO clean this up
|
||||||
|
private suspend fun manageVpn() {
|
||||||
|
networkEventsFlow.collectLatest {
|
||||||
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,17 +28,13 @@ import javax.inject.Inject
|
||||||
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 settingsRepository: SettingsRepository
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||||
lateinit var tunnelConfigRepository: TunnelConfigRepository
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var notificationService: NotificationService
|
||||||
lateinit var notificationService: NotificationService
|
|
||||||
|
|
||||||
private lateinit var job: Job
|
private lateinit var job: Job
|
||||||
|
|
||||||
|
@ -48,7 +44,7 @@ class WireGuardTunnelService : ForegroundService() {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
if(tunnelConfigRepository.getAll().isNotEmpty()) {
|
if (tunnelConfigRepository.getAll().isNotEmpty()) {
|
||||||
launchVpnNotification()
|
launchVpnNotification()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,11 +54,10 @@ class WireGuardTunnelService : ForegroundService() {
|
||||||
super.startService(extras)
|
super.startService(extras)
|
||||||
cancelJob()
|
cancelJob()
|
||||||
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
|
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
|
||||||
val tunnelConfig = tunnelConfigString?.let {
|
val tunnelConfig = tunnelConfigString?.let { TunnelConfig.from(it) }
|
||||||
TunnelConfig.from(it)
|
|
||||||
}
|
|
||||||
tunnelName = tunnelConfig?.name ?: ""
|
tunnelName = tunnelConfig?.name ?: ""
|
||||||
job = lifecycleScope.launch(Dispatchers.IO) {
|
job =
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
launch {
|
launch {
|
||||||
if (tunnelConfig != null) {
|
if (tunnelConfig != null) {
|
||||||
try {
|
try {
|
||||||
|
@ -77,22 +72,22 @@ class WireGuardTunnelService : ForegroundService() {
|
||||||
val settings = settingsRepository.getSettings()
|
val settings = settingsRepository.getSettings()
|
||||||
val tunnels = tunnelConfigRepository.getAll()
|
val tunnels = tunnelConfigRepository.getAll()
|
||||||
if (settings.isAlwaysOnVpnEnabled) {
|
if (settings.isAlwaysOnVpnEnabled) {
|
||||||
val tunnel = if(settings.defaultTunnel != null) {
|
val tunnel =
|
||||||
TunnelConfig.from(settings.defaultTunnel!!)
|
if (settings.defaultTunnel != null) {
|
||||||
} else if(tunnels.isNotEmpty()) {
|
TunnelConfig.from(settings.defaultTunnel!!)
|
||||||
tunnels.first()
|
} else if (tunnels.isNotEmpty()) {
|
||||||
} else {
|
tunnels.first()
|
||||||
null
|
} else {
|
||||||
}
|
null
|
||||||
if(tunnel != null) {
|
}
|
||||||
|
if (tunnel != null) {
|
||||||
tunnelName = tunnel.name
|
tunnelName = tunnel.name
|
||||||
vpnService.startTunnel(tunnel)
|
vpnService.startTunnel(tunnel)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//TODO add failed to connect notification
|
// TODO add failed to connect notification
|
||||||
launch {
|
launch {
|
||||||
vpnService.vpnState.collect { state ->
|
vpnService.vpnState.collect { state ->
|
||||||
state.statistics
|
state.statistics
|
||||||
|
@ -101,14 +96,18 @@ class WireGuardTunnelService : ForegroundService() {
|
||||||
.let { statuses ->
|
.let { statuses ->
|
||||||
when {
|
when {
|
||||||
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
|
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
|
||||||
if(!didShowConnected){
|
if (!didShowConnected) {
|
||||||
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
|
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
|
||||||
launchVpnNotification(getString(R.string.tunnel_start_title),"${getString(R.string.tunnel_start_text)} $tunnelName")
|
launchVpnNotification(
|
||||||
|
getString(R.string.tunnel_start_title),
|
||||||
|
"${getString(R.string.tunnel_start_text)} $tunnelName",
|
||||||
|
)
|
||||||
didShowConnected = true
|
didShowConnected = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
|
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
|
||||||
statuses?.all { it == HandshakeStatus.NOT_STARTED } == true -> {}
|
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
|
||||||
|
true -> {}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -127,7 +126,10 @@ class WireGuardTunnelService : ForegroundService() {
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchVpnNotification(title : String = getString(R.string.vpn_starting),description : String = getString(R.string.attempt_connection)) {
|
private fun launchVpnNotification(
|
||||||
|
title: String = getString(R.string.vpn_starting),
|
||||||
|
description: String = getString(R.string.attempt_connection)
|
||||||
|
) {
|
||||||
val notification =
|
val notification =
|
||||||
notificationService.createNotification(
|
notificationService.createNotification(
|
||||||
channelId = getString(R.string.vpn_channel_id),
|
channelId = getString(R.string.vpn_channel_id),
|
||||||
|
@ -136,13 +138,13 @@ class WireGuardTunnelService : ForegroundService() {
|
||||||
onGoing = false,
|
onGoing = false,
|
||||||
vibration = false,
|
vibration = false,
|
||||||
showTimestamp = true,
|
showTimestamp = true,
|
||||||
description = description
|
description = description,
|
||||||
)
|
)
|
||||||
ServiceCompat.startForeground(
|
ServiceCompat.startForeground(
|
||||||
this,
|
this,
|
||||||
foregroundId,
|
foregroundId,
|
||||||
notification,
|
notification,
|
||||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID
|
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,24 +154,24 @@ class WireGuardTunnelService : ForegroundService() {
|
||||||
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),
|
||||||
action =
|
action =
|
||||||
PendingIntent.getBroadcast(
|
PendingIntent.getBroadcast(
|
||||||
this,
|
this,
|
||||||
0,
|
0,
|
||||||
Intent(this, NotificationActionReceiver::class.java),
|
Intent(this, NotificationActionReceiver::class.java),
|
||||||
PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_IMMUTABLE,
|
||||||
),
|
),
|
||||||
actionText = getString(R.string.restart),
|
actionText = getString(R.string.restart),
|
||||||
title = getString(R.string.vpn_connection_failed),
|
title = getString(R.string.vpn_connection_failed),
|
||||||
onGoing = false,
|
onGoing = false,
|
||||||
vibration = true,
|
vibration = true,
|
||||||
showTimestamp = true,
|
showTimestamp = true,
|
||||||
description = message
|
description = message,
|
||||||
)
|
)
|
||||||
ServiceCompat.startForeground(
|
ServiceCompat.startForeground(
|
||||||
this,
|
this,
|
||||||
foregroundId,
|
foregroundId,
|
||||||
notification,
|
notification,
|
||||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID
|
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,72 +24,69 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
||||||
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 =
|
||||||
val networkStatusCallback =
|
when (Build.VERSION.SDK_INT) {
|
||||||
when (Build.VERSION.SDK_INT) {
|
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
|
||||||
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
|
object :
|
||||||
object : ConnectivityManager.NetworkCallback(
|
ConnectivityManager.NetworkCallback(
|
||||||
FLAG_INCLUDE_LOCATION_INFO
|
FLAG_INCLUDE_LOCATION_INFO,
|
||||||
) {
|
) {
|
||||||
override fun onAvailable(network: Network) {
|
override fun onAvailable(network: Network) {
|
||||||
trySend(NetworkStatus.Available(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(
|
||||||
network: Network,
|
NetworkStatus.CapabilitiesChanged(
|
||||||
networkCapabilities: NetworkCapabilities
|
network,
|
||||||
) {
|
networkCapabilities,
|
||||||
trySend(
|
),
|
||||||
NetworkStatus.CapabilitiesChanged(
|
)
|
||||||
network,
|
|
||||||
networkCapabilities
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val request =
|
else -> {
|
||||||
NetworkRequest.Builder()
|
object : ConnectivityManager.NetworkCallback() {
|
||||||
.addTransportType(networkCapability)
|
override fun onAvailable(network: Network) {
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
trySend(NetworkStatus.Available(network))
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
}
|
||||||
.build()
|
|
||||||
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
|
|
||||||
|
|
||||||
awaitClose {
|
override fun onLost(network: Network) {
|
||||||
connectivityManager.unregisterNetworkCallback(networkStatusCallback)
|
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)
|
||||||
|
@ -119,18 +116,16 @@ 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:
|
||||||
network: Network,
|
suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result
|
||||||
networkCapabilities: NetworkCapabilities
|
): Flow<Result> = map { status ->
|
||||||
) -> Result
|
when (status) {
|
||||||
): Flow<Result> =
|
is NetworkStatus.Unavailable -> onUnavailable(status.network)
|
||||||
map { status ->
|
is NetworkStatus.Available -> onAvailable(status.network)
|
||||||
when (status) {
|
is NetworkStatus.CapabilitiesChanged ->
|
||||||
is NetworkStatus.Unavailable -> onUnavailable(status.network)
|
onCapabilitiesChanged(
|
||||||
is NetworkStatus.Available -> onAvailable(status.network)
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> onCapabilitiesChanged(
|
|
||||||
status.network,
|
status.network,
|
||||||
status.networkCapabilities
|
status.networkCapabilities,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,9 +5,5 @@ 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
|
|
||||||
constructor(
|
|
||||||
@ApplicationContext context: Context
|
|
||||||
) :
|
|
||||||
BaseNetworkService<EthernetService>(context, NetworkCapabilities.TRANSPORT_ETHERNET)
|
BaseNetworkService<EthernetService>(context, NetworkCapabilities.TRANSPORT_ETHERNET)
|
||||||
|
|
|
@ -5,9 +5,5 @@ 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
|
|
||||||
constructor(
|
|
||||||
@ApplicationContext context: Context
|
|
||||||
) :
|
|
||||||
BaseNetworkService<MobileDataService>(context, NetworkCapabilities.TRANSPORT_CELLULAR)
|
BaseNetworkService<MobileDataService>(context, NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||||
|
|
|
@ -5,9 +5,5 @@ 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
|
|
||||||
constructor(
|
|
||||||
@ApplicationContext context: Context
|
|
||||||
) :
|
|
||||||
BaseNetworkService<WifiService>(context, NetworkCapabilities.TRANSPORT_WIFI)
|
BaseNetworkService<WifiService>(context, NetworkCapabilities.TRANSPORT_WIFI)
|
||||||
|
|
|
@ -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,23 +13,21 @@ 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) :
|
||||||
@Inject
|
NotificationService {
|
||||||
constructor(
|
|
||||||
@ApplicationContext private val context: Context
|
|
||||||
) : NotificationService {
|
|
||||||
private val notificationManager =
|
private val notificationManager =
|
||||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
private val watcherBuilder: NotificationCompat.Builder =
|
private val watcherBuilder: NotificationCompat.Builder =
|
||||||
NotificationCompat.Builder(
|
NotificationCompat.Builder(
|
||||||
context,
|
context,
|
||||||
context.getString(R.string.watcher_channel_id)
|
context.getString(R.string.watcher_channel_id),
|
||||||
|
)
|
||||||
|
private val tunnelBuilder: NotificationCompat.Builder =
|
||||||
|
NotificationCompat.Builder(
|
||||||
|
context,
|
||||||
|
context.getString(R.string.vpn_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,
|
||||||
|
@ -47,17 +45,18 @@ constructor(
|
||||||
): Notification {
|
): Notification {
|
||||||
val channel =
|
val channel =
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
channelId,
|
channelId,
|
||||||
channelName,
|
channelName,
|
||||||
importance
|
importance,
|
||||||
).let {
|
)
|
||||||
it.description = title
|
.let {
|
||||||
it.enableLights(lights)
|
it.description = title
|
||||||
it.lightColor = Color.RED
|
it.enableLights(lights)
|
||||||
it.enableVibration(vibration)
|
it.lightColor = Color.RED
|
||||||
it.vibrationPattern = longArrayOf(100,200,300)
|
it.enableVibration(vibration)
|
||||||
it
|
it.vibrationPattern = longArrayOf(100, 200, 300)
|
||||||
}
|
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 ->
|
||||||
|
@ -65,26 +64,26 @@ constructor(
|
||||||
context,
|
context,
|
||||||
0,
|
0,
|
||||||
notificationIntent,
|
notificationIntent,
|
||||||
PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_IMMUTABLE,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val builder = when(channelId) {
|
val builder =
|
||||||
context.getString(R.string.watcher_channel_id) -> watcherBuilder
|
when (channelId) {
|
||||||
context.getString(R.string.vpn_channel_id) -> tunnelBuilder
|
context.getString(R.string.watcher_channel_id) -> watcherBuilder
|
||||||
else -> {
|
context.getString(R.string.vpn_channel_id) -> tunnelBuilder
|
||||||
NotificationCompat.Builder(
|
else -> {
|
||||||
context,
|
NotificationCompat.Builder(
|
||||||
channelId
|
context,
|
||||||
)
|
channelId,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return builder.let {
|
return builder.let {
|
||||||
if (action != null && actionText != null) {
|
if (action != null && actionText != null) {
|
||||||
it.addAction(
|
it.addAction(
|
||||||
NotificationCompat.Action.Builder(0, actionText, action)
|
NotificationCompat.Action.Builder(0, actionText, action).build(),
|
||||||
.build()
|
|
||||||
)
|
)
|
||||||
it.setAutoCancel(true)
|
it.setAutoCancel(true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,33 +12,34 @@ import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
||||||
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
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
lateinit var settingsRepository: SettingsRepository
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||||
lateinit var tunnelConfigRepository: TunnelConfigRepository
|
|
||||||
|
|
||||||
private suspend fun toggleWatcherServicePause() {
|
private suspend fun toggleWatcherServicePause() {
|
||||||
val settings = settingsRepository.getSettings()
|
val settings = settingsRepository.getSettings()
|
||||||
if (settings.isAutoTunnelEnabled) {
|
if (settings.isAutoTunnelEnabled) {
|
||||||
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
||||||
settingsRepository.save(settings.copy(
|
settingsRepository.save(
|
||||||
isAutoTunnelPaused = pauseAutoTunnel
|
settings.copy(
|
||||||
))
|
isAutoTunnelPaused = pauseAutoTunnel,
|
||||||
}
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(View(this))
|
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) {
|
||||||
|
@ -48,7 +49,9 @@ class ShortcutsActivity : ComponentActivity() {
|
||||||
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) {
|
||||||
tunnelConfigRepository.getAll().firstOrNull { it.name == tunnelName }
|
tunnelConfigRepository.getAll().firstOrNull {
|
||||||
|
it.name == tunnelName
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (settings.defaultTunnel == null) {
|
if (settings.defaultTunnel == null) {
|
||||||
tunnelConfigRepository.getAll().first()
|
tunnelConfigRepository.getAll().first()
|
||||||
|
@ -59,13 +62,15 @@ class ShortcutsActivity : ComponentActivity() {
|
||||||
tunnelConfig ?: return@launch
|
tunnelConfig ?: return@launch
|
||||||
toggleWatcherServicePause()
|
toggleWatcherServicePause()
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
Action.STOP.name -> ServiceManager.stopVpnService(
|
Action.STOP.name ->
|
||||||
this@ShortcutsActivity
|
ServiceManager.stopVpnService(
|
||||||
)
|
this@ShortcutsActivity,
|
||||||
Action.START.name -> ServiceManager.startVpnServiceForeground(
|
)
|
||||||
this@ShortcutsActivity,
|
Action.START.name ->
|
||||||
tunnelConfig.toString()
|
ServiceManager.startVpnServiceForeground(
|
||||||
)
|
this@ShortcutsActivity,
|
||||||
|
tunnelConfig.toString(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e.message)
|
Timber.e(e.message)
|
||||||
|
|
|
@ -20,44 +20,43 @@ import javax.inject.Inject
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class TunnelControlTile() : TileService() {
|
class TunnelControlTile() : TileService() {
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||||
lateinit var tunnelConfigRepository: TunnelConfigRepository
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
lateinit var settingsRepository: SettingsRepository
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var vpnService: VpnService
|
||||||
lateinit var vpnService: VpnService
|
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO)
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
private var tunnelName : String? = null
|
private var tunnelName: String? = null
|
||||||
|
|
||||||
override fun onStartListening() {
|
override fun onStartListening() {
|
||||||
super.onStartListening()
|
super.onStartListening()
|
||||||
Timber.d("On start listening called")
|
Timber.d("On start listening called")
|
||||||
scope.launch {
|
scope.launch {
|
||||||
vpnService.vpnState.collect {
|
vpnService.vpnState.collect {
|
||||||
when(it.status) {
|
when (it.status) {
|
||||||
Tunnel.State.UP -> setActive()
|
Tunnel.State.UP -> setActive()
|
||||||
Tunnel.State.DOWN -> setInactive()
|
Tunnel.State.DOWN -> setInactive()
|
||||||
else -> setInactive()
|
else -> setInactive()
|
||||||
}
|
}
|
||||||
val tunnels = tunnelConfigRepository.getAll()
|
val tunnels = tunnelConfigRepository.getAll()
|
||||||
if(tunnels.isEmpty()) {
|
if (tunnels.isEmpty()) {
|
||||||
setUnavailable()
|
setUnavailable()
|
||||||
return@collect
|
return@collect
|
||||||
}
|
}
|
||||||
tunnelName = it.name.ifBlank {
|
tunnelName =
|
||||||
val settings = settingsRepository.getSettings()
|
it.name.ifBlank {
|
||||||
if (settings.defaultTunnel != null) {
|
val settings = settingsRepository.getSettings()
|
||||||
TunnelConfig.from(settings.defaultTunnel!!).name
|
if (settings.defaultTunnel != null) {
|
||||||
} else tunnels.firstOrNull()?.name
|
TunnelConfig.from(settings.defaultTunnel!!).name
|
||||||
}
|
} else tunnels.firstOrNull()?.name
|
||||||
|
}
|
||||||
setTileDescription(tunnelName ?: "")
|
setTileDescription(tunnelName ?: "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
|
@ -73,14 +72,15 @@ class TunnelControlTile() : TileService() {
|
||||||
unlockAndRun {
|
unlockAndRun {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
val tunnelConfig = tunnelConfigRepository.getAll().first { it.name == tunnelName }
|
val tunnelConfig =
|
||||||
|
tunnelConfigRepository.getAll().first { it.name == tunnelName }
|
||||||
toggleWatcherServicePause()
|
toggleWatcherServicePause()
|
||||||
if (vpnService.getState() == Tunnel.State.UP) {
|
if (vpnService.getState() == Tunnel.State.UP) {
|
||||||
ServiceManager.stopVpnService(this@TunnelControlTile)
|
ServiceManager.stopVpnService(this@TunnelControlTile)
|
||||||
} else {
|
} else {
|
||||||
ServiceManager.startVpnServiceForeground(
|
ServiceManager.startVpnServiceForeground(
|
||||||
this@TunnelControlTile,
|
this@TunnelControlTile,
|
||||||
tunnelConfig.toString()
|
tunnelConfig.toString(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -97,9 +97,11 @@ class TunnelControlTile() : TileService() {
|
||||||
val settings = settingsRepository.getSettings()
|
val settings = settingsRepository.getSettings()
|
||||||
if (settings.isAutoTunnelEnabled) {
|
if (settings.isAutoTunnelEnabled) {
|
||||||
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
||||||
settingsRepository.save(settings.copy(
|
settingsRepository.save(
|
||||||
isAutoTunnelPaused = pauseAutoTunnel
|
settings.copy(
|
||||||
))
|
isAutoTunnelPaused = pauseAutoTunnel,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,13 +4,13 @@ enum class HandshakeStatus {
|
||||||
HEALTHY,
|
HEALTHY,
|
||||||
STALE,
|
STALE,
|
||||||
UNKNOWN,
|
UNKNOWN,
|
||||||
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 = 180
|
||||||
const val STATUS_CHANGE_TIME_BUFFER = 30
|
const val STATUS_CHANGE_TIME_BUFFER = 30
|
||||||
const val STALE_TIME_LIMIT_SEC = WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC + STATUS_CHANGE_TIME_BUFFER
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import com.wireguard.android.backend.Statistics
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
|
|
||||||
data class VpnState(
|
data class VpnState(
|
||||||
val status : Tunnel.State = Tunnel.State.DOWN,
|
val status: Tunnel.State = Tunnel.State.DOWN,
|
||||||
val name : String = "",
|
val name: String = "",
|
||||||
val statistics : Statistics? = null
|
val statistics: Statistics? = null
|
||||||
)
|
)
|
||||||
|
|
|
@ -67,7 +67,7 @@ constructor(
|
||||||
backend.setState(
|
backend.setState(
|
||||||
this,
|
this,
|
||||||
State.UP,
|
State.UP,
|
||||||
config
|
config,
|
||||||
)
|
)
|
||||||
emitTunnelState(state)
|
emitTunnelState(state)
|
||||||
state
|
state
|
||||||
|
@ -80,24 +80,24 @@ constructor(
|
||||||
private fun emitTunnelState(state: State) {
|
private fun emitTunnelState(state: State) {
|
||||||
_vpnState.tryEmit(
|
_vpnState.tryEmit(
|
||||||
_vpnState.value.copy(
|
_vpnState.value.copy(
|
||||||
status = state
|
status = state,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun emitBackendStatistics(statistics: Statistics) {
|
private fun emitBackendStatistics(statistics: Statistics) {
|
||||||
_vpnState.tryEmit(
|
_vpnState.tryEmit(
|
||||||
_vpnState.value.copy(
|
_vpnState.value.copy(
|
||||||
statistics = statistics
|
statistics = statistics,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun emitTunnelName(name: String) {
|
private suspend fun emitTunnelName(name: String) {
|
||||||
_vpnState.emit(
|
_vpnState.emit(
|
||||||
_vpnState.value.copy(
|
_vpnState.value.copy(
|
||||||
name = name
|
name = name,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,8 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ActivityViewModel @Inject constructor(
|
class ActivityViewModel
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
private val settingsRepo: SettingsDao,
|
private val settingsRepo: SettingsDao,
|
||||||
) : ViewModel() {
|
) : ViewModel() {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ 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.focusProperties
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
|
@ -37,6 +38,9 @@ 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.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
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
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
|
||||||
|
@ -50,19 +54,40 @@ 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
|
||||||
|
import java.io.IOException
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var dataStoreManager: DataStoreManager
|
||||||
|
|
||||||
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
@OptIn(
|
@OptIn(
|
||||||
ExperimentalPermissionsApi::class
|
ExperimentalPermissionsApi::class,
|
||||||
)
|
)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// load preferences into memory and init data
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
dataStoreManager.init()
|
||||||
|
if (settingsRepository.getAll().isEmpty()) {
|
||||||
|
settingsRepository.save(com.zaneschepke.wireguardautotunnel.data.model.Settings())
|
||||||
|
}
|
||||||
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.e("Failed to load preferences")
|
||||||
|
}
|
||||||
|
}
|
||||||
setContent {
|
setContent {
|
||||||
// val activityViewModel = hiltViewModel<ActivityViewModel>()
|
// val activityViewModel = hiltViewModel<ActivityViewModel>()
|
||||||
|
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val focusRequester = remember { FocusRequester()}
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
WireguardAutoTunnelTheme {
|
WireguardAutoTunnelTheme {
|
||||||
TransparentSystemBars()
|
TransparentSystemBars()
|
||||||
|
@ -73,7 +98,10 @@ class MainActivity : AppCompatActivity() {
|
||||||
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
|
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
|
||||||
fun requestNotificationPermission() {
|
fun requestNotificationPermission() {
|
||||||
if (!notificationPermissionState.status.isGranted && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (
|
||||||
|
!notificationPermissionState.status.isGranted &&
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
|
||||||
|
) {
|
||||||
notificationPermissionState.launchPermissionRequest()
|
notificationPermissionState.launchPermissionRequest()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,7 +115,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
if (accepted) {
|
if (accepted) {
|
||||||
vpnIntent = null
|
vpnIntent = null
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
LaunchedEffect(vpnIntent) {
|
LaunchedEffect(vpnIntent) {
|
||||||
if (vpnIntent != null) {
|
if (vpnIntent != null) {
|
||||||
|
@ -99,13 +127,15 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
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,
|
||||||
|
SnackbarResult.Dismissed -> {
|
||||||
snackbarHostState.currentSnackbarData?.dismiss()
|
snackbarHostState.currentSnackbarData?.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -118,29 +148,36 @@ class MainActivity : AppCompatActivity() {
|
||||||
CustomSnackBar(
|
CustomSnackBar(
|
||||||
snackbarData.visuals.message,
|
snackbarData.visuals.message,
|
||||||
isRtl = false,
|
isRtl = false,
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
|
containerColor =
|
||||||
2.dp
|
MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||||
)
|
2.dp,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.focusable().focusProperties { up = focusRequester },
|
modifier = Modifier.focusable().focusProperties { up = focusRequester },
|
||||||
bottomBar =
|
bottomBar =
|
||||||
if (vpnIntent == null && notificationPermissionState.status.isGranted) {
|
if (vpnIntent == null && notificationPermissionState.status.isGranted) {
|
||||||
{ BottomNavBar(navController, listOf(
|
{
|
||||||
Screen.Main.navItem,
|
BottomNavBar(
|
||||||
Screen.Settings.navItem,
|
navController,
|
||||||
Screen.Support.navItem)) }
|
listOf(
|
||||||
} else {
|
Screen.Main.navItem,
|
||||||
{}
|
Screen.Settings.navItem,
|
||||||
}
|
Screen.Support.navItem,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
{}
|
||||||
|
},
|
||||||
) { padding ->
|
) { padding ->
|
||||||
if (vpnIntent != null) {
|
if (vpnIntent != null) {
|
||||||
PermissionRequestFailedScreen(
|
PermissionRequestFailedScreen(
|
||||||
padding = padding,
|
padding = padding,
|
||||||
onRequestAgain = { vpnActivityResultState.launch(vpnIntent) },
|
onRequestAgain = { vpnActivityResultState.launch(vpnIntent) },
|
||||||
message = getString(R.string.vpn_permission_required),
|
message = getString(R.string.vpn_permission_required),
|
||||||
getString(R.string.retry)
|
getString(R.string.retry),
|
||||||
)
|
)
|
||||||
return@Scaffold
|
return@Scaffold
|
||||||
}
|
}
|
||||||
|
@ -154,12 +191,12 @@ class MainActivity : AppCompatActivity() {
|
||||||
Uri.fromParts(
|
Uri.fromParts(
|
||||||
Constants.URI_PACKAGE_SCHEME,
|
Constants.URI_PACKAGE_SCHEME,
|
||||||
this.packageName,
|
this.packageName,
|
||||||
null
|
null,
|
||||||
)
|
)
|
||||||
startActivity(intentSettings)
|
startActivity(intentSettings)
|
||||||
},
|
},
|
||||||
message = getString(R.string.notification_permission_required),
|
message = getString(R.string.notification_permission_required),
|
||||||
getString(R.string.open_settings)
|
getString(R.string.open_settings),
|
||||||
)
|
)
|
||||||
return@Scaffold
|
return@Scaffold
|
||||||
}
|
}
|
||||||
|
@ -167,34 +204,42 @@ class MainActivity : AppCompatActivity() {
|
||||||
composable(
|
composable(
|
||||||
Screen.Main.route,
|
Screen.Main.route,
|
||||||
) {
|
) {
|
||||||
MainScreen(padding = padding, focusRequester = focusRequester, showSnackbarMessage = { message ->
|
MainScreen(
|
||||||
showSnackBarMessage(message)
|
padding = padding,
|
||||||
}, navController = navController)
|
focusRequester = focusRequester,
|
||||||
|
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
||||||
|
navController = navController,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
composable(Screen.Settings.route,
|
composable(
|
||||||
|
Screen.Settings.route,
|
||||||
) {
|
) {
|
||||||
SettingsScreen(padding = padding, showSnackbarMessage = { message ->
|
SettingsScreen(
|
||||||
showSnackBarMessage(message)
|
padding = padding,
|
||||||
}, focusRequester = focusRequester)
|
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
||||||
|
focusRequester = focusRequester,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
composable(Screen.Support.route,
|
composable(
|
||||||
|
Screen.Support.route,
|
||||||
) {
|
) {
|
||||||
SupportScreen(padding = padding, focusRequester = focusRequester,
|
SupportScreen(
|
||||||
showSnackbarMessage = { message ->
|
padding = padding,
|
||||||
showSnackBarMessage(message)
|
focusRequester = focusRequester,
|
||||||
})
|
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
composable("${Screen.Config.route}/{id}") {
|
composable("${Screen.Config.route}/{id}") {
|
||||||
val id = it.arguments?.getString("id")
|
val id = it.arguments?.getString("id")
|
||||||
if (!id.isNullOrBlank()) {
|
if (!id.isNullOrBlank()) {
|
||||||
//https://dagger.dev/hilt/view-model#assisted-injection
|
// https://dagger.dev/hilt/view-model#assisted-injection
|
||||||
ConfigScreen(
|
ConfigScreen(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
id = id,
|
id = id,
|
||||||
showSnackbarMessage = { message ->
|
showSnackbarMessage = { message ->
|
||||||
showSnackBarMessage(message)
|
showSnackBarMessage(message)
|
||||||
},
|
},
|
||||||
focusRequester = focusRequester
|
focusRequester = focusRequester,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,28 +6,33 @@ import androidx.compose.material.icons.rounded.QuestionMark
|
||||||
import androidx.compose.material.icons.rounded.Settings
|
import androidx.compose.material.icons.rounded.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
|
||||||
|
|
||||||
sealed class Screen(val route : String) {
|
sealed class Screen(val route: String) {
|
||||||
data object Main: Screen("main") {
|
data object Main : Screen("main") {
|
||||||
val navItem = BottomNavItem(
|
val navItem =
|
||||||
name = "Tunnels",
|
BottomNavItem(
|
||||||
route = route,
|
name = "Tunnels",
|
||||||
icon = Icons.Rounded.Home
|
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")
|
|
||||||
|
|
||||||
}
|
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")
|
||||||
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ fun ClickableIconButton(
|
||||||
) {
|
) {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
enabled = enabled
|
enabled = enabled,
|
||||||
) {
|
) {
|
||||||
Text(text, Modifier.weight(1f, false))
|
Text(text, Modifier.weight(1f, false))
|
||||||
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
|
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
|
||||||
|
@ -31,11 +31,11 @@ fun ClickableIconButton(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = stringResource(R.string.delete),
|
contentDescription = stringResource(R.string.delete),
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.size(ButtonDefaults.IconSize).weight(1f, false).clickable {
|
Modifier.size(ButtonDefaults.IconSize).weight(1f, false).clickable {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
onIconClick()
|
onIconClick()
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,17 +26,12 @@ fun PermissionRequestFailedScreen(
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
modifier =
|
modifier = Modifier.fillMaxSize().padding(padding),
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.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(
|
||||||
scope.launch {
|
onClick = { scope.launch { onRequestAgain() } },
|
||||||
onRequestAgain()
|
) {
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Text(buttonText)
|
Text(buttonText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,30 +34,22 @@ fun RowListItem(
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.animateContentSize()
|
||||||
.animateContentSize()
|
.clip(RoundedCornerShape(30.dp))
|
||||||
.clip(RoundedCornerShape(30.dp))
|
.combinedClickable(
|
||||||
.combinedClickable(
|
onClick = { onClick() },
|
||||||
onClick = {
|
onLongClick = { onHold() },
|
||||||
onClick()
|
),
|
||||||
},
|
|
||||||
onLongClick = {
|
|
||||||
onHold()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 15.dp, vertical = 5.dp),
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 15.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)
|
modifier = Modifier.fillMaxWidth(.60f),
|
||||||
) {
|
) {
|
||||||
icon()
|
icon()
|
||||||
Text(text)
|
Text(text)
|
||||||
|
@ -68,11 +60,10 @@ fun RowListItem(
|
||||||
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,
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
) {
|
) {
|
||||||
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
|
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
|
||||||
val peerTx = statistics.peer(it)!!.txBytes
|
val peerTx = statistics.peer(it)!!.txBytes
|
||||||
|
|
|
@ -47,7 +47,7 @@ fun SearchBar(onQuery: (queryString: String) -> Unit) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.Search,
|
imageVector = Icons.Rounded.Search,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
contentDescription = stringResource(id = R.string.search_icon)
|
contentDescription = stringResource(id = R.string.search_icon),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
|
@ -56,25 +56,24 @@ fun SearchBar(onQuery: (queryString: String) -> Unit) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.Clear,
|
imageVector = Icons.Rounded.Clear,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
contentDescription = stringResource(id = R.string.clear_icon)
|
contentDescription = stringResource(id = R.string.clear_icon),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
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)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,19 +22,15 @@ fun ConfigurationTextBox(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
value = value,
|
value = value,
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
onValueChange = {
|
onValueChange = { onValueChange(it) },
|
||||||
onValueChange(it)
|
|
||||||
},
|
|
||||||
label = { Text(label) },
|
label = { Text(label) },
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,21 +21,16 @@ fun ConfigurationToggle(
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier = Modifier.fillMaxWidth().padding(padding),
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(padding),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
Text(label)
|
Text(label)
|
||||||
Switch(
|
Switch(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
checked = checked,
|
checked = checked,
|
||||||
onCheckedChange = {
|
onCheckedChange = { onCheckChanged() },
|
||||||
onCheckChanged()
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,14 +11,11 @@ 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,15 +26,15 @@ 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",
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,51 +11,40 @@ 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) }
|
||||||
|
|
||||||
|
@ -71,10 +60,7 @@ fun AuthorizationPrompt(
|
||||||
context as FragmentActivity,
|
context as FragmentActivity,
|
||||||
executor,
|
executor,
|
||||||
object : BiometricPrompt.AuthenticationCallback() {
|
object : BiometricPrompt.AuthenticationCallback() {
|
||||||
override fun onAuthenticationError(
|
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||||
errorCode: Int,
|
|
||||||
errString: CharSequence
|
|
||||||
) {
|
|
||||||
super.onAuthenticationError(errorCode, errString)
|
super.onAuthenticationError(errorCode, errString)
|
||||||
onFailure()
|
onFailure()
|
||||||
}
|
}
|
||||||
|
@ -90,7 +76,7 @@ fun AuthorizationPrompt(
|
||||||
super.onAuthenticationFailed()
|
super.onAuthenticationFailed()
|
||||||
onFailure()
|
onFailure()
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
biometricPrompt.authenticate(promptInfo)
|
biometricPrompt.authenticate(promptInfo)
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,25 +37,25 @@ fun CustomSnackBar(
|
||||||
Snackbar(
|
Snackbar(
|
||||||
containerColor = containerColor,
|
containerColor = containerColor,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth(
|
Modifier.fillMaxWidth(
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f,
|
||||||
).padding(bottom = 100.dp),
|
)
|
||||||
shape = RoundedCornerShape(16.dp)
|
.padding(bottom = 100.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
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.width(IntrinsicSize.Max).height(IntrinsicSize.Min),
|
modifier = Modifier.width(IntrinsicSize.Max).height(IntrinsicSize.Min),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Start
|
horizontalArrangement = Arrangement.Start,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Info,
|
Icons.Rounded.Info,
|
||||||
contentDescription = stringResource(R.string.info),
|
contentDescription = stringResource(R.string.info),
|
||||||
tint = Color.White,
|
tint = Color.White,
|
||||||
modifier = Modifier.padding(end = 10.dp)
|
modifier = Modifier.padding(end = 10.dp),
|
||||||
)
|
)
|
||||||
Text(message, color = Color.White, modifier = Modifier.padding(end = 5.dp))
|
Text(message, color = Color.White, modifier = Modifier.padding(end = 5.dp))
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,10 +13,11 @@ import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LoadingScreen() {
|
fun LoadingScreen() {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier.fillMaxSize().focusable().padding()) {
|
modifier = Modifier.fillMaxSize().focusable().padding(),
|
||||||
Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() }
|
) {
|
||||||
|
Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,14 +12,11 @@ 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,
|
||||||
style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.ExtraBold),
|
style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.ExtraBold),
|
||||||
modifier = Modifier.padding(padding, bottom = 5.dp, top = 5.dp)
|
modifier = Modifier.padding(padding, bottom = 5.dp, top = 5.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,13 +17,13 @@ data class InterfaceProxy(
|
||||||
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 =
|
||||||
i.listenPort.get().toString()
|
if (i.listenPort.isPresent) {
|
||||||
.trim()
|
i.listenPort.get().toString().trim()
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
},
|
},
|
||||||
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else ""
|
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,36 +13,60 @@ data class PeerProxy(
|
||||||
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 =
|
||||||
peer.preSharedKey.get().toBase64()
|
if (peer.preSharedKey.isPresent) {
|
||||||
.trim()
|
peer.preSharedKey.get().toBase64().trim()
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
},
|
},
|
||||||
persistentKeepalive = if (peer.persistentKeepalive.isPresent) {
|
persistentKeepalive =
|
||||||
peer.persistentKeepalive.get()
|
if (peer.persistentKeepalive.isPresent) {
|
||||||
.toString().trim()
|
peer.persistentKeepalive.get().toString().trim()
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
},
|
},
|
||||||
endpoint = if (peer.endpoint.isPresent) {
|
endpoint =
|
||||||
peer.endpoint.get().toString()
|
if (peer.endpoint.isPresent) {
|
||||||
.trim()
|
peer.endpoint.get().toString().trim()
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
},
|
},
|
||||||
allowedIps = peer.allowedIps.joinToString(", ").trim()
|
allowedIps = peer.allowedIps.joinToString(", ").trim(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val IPV4_PUBLIC_NETWORKS =
|
val IPV4_PUBLIC_NETWORKS =
|
||||||
setOf(
|
setOf(
|
||||||
"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",
|
"0.0.0.0/5",
|
||||||
"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",
|
"8.0.0.0/7",
|
||||||
"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",
|
"11.0.0.0/8",
|
||||||
"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",
|
"12.0.0.0/6",
|
||||||
"192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10",
|
"16.0.0.0/4",
|
||||||
"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"
|
"32.0.0.0/3",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,7 +88,8 @@ import kotlinx.coroutines.delay
|
||||||
@OptIn(
|
@OptIn(
|
||||||
ExperimentalComposeUiApi::class,
|
ExperimentalComposeUiApi::class,
|
||||||
ExperimentalMaterial3Api::class,
|
ExperimentalMaterial3Api::class,
|
||||||
ExperimentalFoundationApi::class)
|
ExperimentalFoundationApi::class,
|
||||||
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun ConfigScreen(
|
fun ConfigScreen(
|
||||||
viewModel: ConfigViewModel = hiltViewModel(),
|
viewModel: ConfigViewModel = hiltViewModel(),
|
||||||
|
@ -97,347 +98,374 @@ fun ConfigScreen(
|
||||||
showSnackbarMessage: (String) -> Unit,
|
showSnackbarMessage: (String) -> Unit,
|
||||||
id: String
|
id: String
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
var showApplicationsDialog by remember { mutableStateOf(false) }
|
var showApplicationsDialog by remember { mutableStateOf(false) }
|
||||||
var showAuthPrompt by remember { mutableStateOf(false) }
|
var showAuthPrompt by remember { mutableStateOf(false) }
|
||||||
var isAuthenticated by remember { mutableStateOf(false) }
|
var isAuthenticated by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) { viewModel.init(id) }
|
||||||
viewModel.init(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(uiState.loading) {
|
LaunchedEffect(uiState.loading) {
|
||||||
if(!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
delay(Constants.FOCUS_REQUEST_DELAY)
|
delay(Constants.FOCUS_REQUEST_DELAY)
|
||||||
focusRequester.requestFocus()
|
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) {
|
if (uiState.loading) {
|
||||||
AuthorizationPrompt(
|
LoadingScreen()
|
||||||
onSuccess = {
|
return
|
||||||
showAuthPrompt = false
|
}
|
||||||
isAuthenticated = true
|
|
||||||
},
|
|
||||||
onError = { error ->
|
|
||||||
showAuthPrompt = false
|
|
||||||
showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
|
||||||
},
|
|
||||||
onFailure = {
|
|
||||||
showAuthPrompt = false
|
|
||||||
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showApplicationsDialog) {
|
val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() })
|
||||||
val sortedPackages =
|
|
||||||
remember(uiState.packages) { uiState.packages.sortedBy { viewModel.getPackageLabel(it) } }
|
val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done)
|
||||||
AlertDialog(onDismissRequest = { showApplicationsDialog = false }) {
|
|
||||||
Surface(
|
val fillMaxHeight = .85f
|
||||||
tonalElevation = 2.dp,
|
val fillMaxWidth = .85f
|
||||||
shadowElevation = 2.dp,
|
val screenPadding = 5.dp
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
color = MaterialTheme.colorScheme.surface,
|
val applicationButtonText = {
|
||||||
modifier =
|
"Tunneling apps: " +
|
||||||
Modifier.fillMaxWidth()
|
if (uiState.isAllApplicationsEnabled) {
|
||||||
.fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f)) {
|
"all"
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
} else {
|
||||||
Row(
|
"${uiState.checkedPackageNames.size} " +
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp),
|
(if (uiState.include) "included" else "excluded")
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
}
|
||||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
}
|
||||||
Text(stringResource(id = R.string.tunnel_all))
|
|
||||||
Switch(
|
if (showAuthPrompt) {
|
||||||
checked = uiState.isAllApplicationsEnabled,
|
AuthorizationPrompt(
|
||||||
onCheckedChange = { viewModel.onAllApplicationsChange(it) })
|
onSuccess = {
|
||||||
}
|
showAuthPrompt = false
|
||||||
if (!uiState.isAllApplicationsEnabled) {
|
isAuthenticated = true
|
||||||
Row(
|
},
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp),
|
onError = { error ->
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
showAuthPrompt = false
|
||||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||||
Row(
|
},
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
onFailure = {
|
||||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
showAuthPrompt = false
|
||||||
Text(stringResource(id = R.string.include))
|
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||||
Checkbox(
|
},
|
||||||
checked = uiState.include,
|
)
|
||||||
onCheckedChange = { viewModel.onIncludeChange(!uiState.include) })
|
}
|
||||||
}
|
|
||||||
Row(
|
if (showApplicationsDialog) {
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
val sortedPackages =
|
||||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
remember(uiState.packages) {
|
||||||
Text(stringResource(id = R.string.exclude))
|
uiState.packages.sortedBy { viewModel.getPackageLabel(it) }
|
||||||
Checkbox(
|
}
|
||||||
checked = !uiState.include,
|
AlertDialog(onDismissRequest = { showApplicationsDialog = false }) {
|
||||||
onCheckedChange = { viewModel.onIncludeChange(!uiState.include) })
|
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) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Row(
|
if (!uiState.isAllApplicationsEnabled) {
|
||||||
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(
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp, vertical = 7.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
modifier = Modifier.fillMaxSize().padding(5.dp)) {
|
) {
|
||||||
Row(modifier = Modifier.fillMaxWidth(fillMaxWidth)) {
|
Row(
|
||||||
val drawable =
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
pack.applicationInfo?.loadIcon(context.packageManager)
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
if (drawable != null) {
|
) {
|
||||||
Image(
|
Text(stringResource(id = R.string.include))
|
||||||
painter = DrawablePainter(drawable),
|
Checkbox(
|
||||||
stringResource(id = R.string.icon),
|
checked = uiState.include,
|
||||||
modifier = Modifier.size(50.dp, 50.dp))
|
onCheckedChange = {
|
||||||
} else {
|
viewModel.onIncludeChange(!uiState.include)
|
||||||
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,
|
||||||
|
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(
|
||||||
Row(
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
||||||
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
horizontalArrangement = Arrangement.Center,
|
||||||
horizontalArrangement = Arrangement.Center) {
|
) {
|
||||||
TextButton(onClick = { showApplicationsDialog = false }) {
|
TextButton(onClick = { showApplicationsDialog = false }) {
|
||||||
Text(stringResource(R.string.done))
|
Text(stringResource(R.string.done))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
floatingActionButtonPosition = FabPosition.End,
|
floatingActionButtonPosition = FabPosition.End,
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
val secondaryColor = MaterialTheme.colorScheme.secondary
|
val secondaryColor = MaterialTheme.colorScheme.secondary
|
||||||
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||||
var fobColor by remember { mutableStateOf(secondaryColor) }
|
var fobColor by remember { mutableStateOf(secondaryColor) }
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.padding(bottom = 90.dp).onFocusChanged {
|
Modifier.padding(bottom = 90.dp).onFocusChanged {
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
viewModel.onSaveAllChanges().let {
|
||||||
|
when (it) {
|
||||||
|
is Result.Success -> {
|
||||||
|
showSnackbarMessage(it.data.message)
|
||||||
|
navController.navigate(Screen.Main.route)
|
||||||
|
}
|
||||||
|
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onClick = {
|
containerColor = fobColor,
|
||||||
viewModel.onSaveAllChanges().let {
|
shape = RoundedCornerShape(16.dp),
|
||||||
when (it) {
|
) {
|
||||||
is Result.Success -> {
|
|
||||||
showSnackbarMessage(it.data.message)
|
|
||||||
navController.navigate(Screen.Main.route)
|
|
||||||
}
|
|
||||||
is Result.Error -> showSnackbarMessage(it.error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
containerColor = fobColor,
|
|
||||||
shape = RoundedCornerShape(16.dp)) {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.Save,
|
imageVector = Icons.Rounded.Save,
|
||||||
contentDescription = stringResource(id = R.string.save_changes),
|
contentDescription = stringResource(id = R.string.save_changes),
|
||||||
tint = Color.DarkGray)
|
tint = Color.DarkGray,
|
||||||
}
|
)
|
||||||
}) {
|
}
|
||||||
Column {
|
},
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.verticalScroll(rememberScrollState()).weight(1f, true).fillMaxSize()) {
|
Modifier.verticalScroll(rememberScrollState()).weight(1f, true).fillMaxSize(),
|
||||||
Surface(
|
) {
|
||||||
tonalElevation = 2.dp,
|
Surface(
|
||||||
shadowElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
shape = RoundedCornerShape(12.dp),
|
shadowElevation = 2.dp,
|
||||||
color = MaterialTheme.colorScheme.surface,
|
shape = RoundedCornerShape(12.dp),
|
||||||
modifier =
|
color = MaterialTheme.colorScheme.surface,
|
||||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
modifier =
|
||||||
|
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Modifier.fillMaxHeight(fillMaxHeight).fillMaxWidth(fillMaxWidth)
|
Modifier.fillMaxHeight(fillMaxHeight).fillMaxWidth(fillMaxWidth)
|
||||||
} else {
|
} else {
|
||||||
Modifier.fillMaxWidth(fillMaxWidth)
|
Modifier.fillMaxWidth(fillMaxWidth)
|
||||||
})
|
})
|
||||||
.padding(top = 50.dp, bottom = 10.dp)) {
|
.padding(top = 50.dp, bottom = 10.dp),
|
||||||
Column(
|
) {
|
||||||
horizontalAlignment = Alignment.Start,
|
Column(
|
||||||
verticalArrangement = Arrangement.Top,
|
horizontalAlignment = Alignment.Start,
|
||||||
modifier = Modifier.padding(15.dp).focusGroup()) {
|
verticalArrangement = Arrangement.Top,
|
||||||
SectionTitle(
|
modifier = Modifier.padding(15.dp).focusGroup(),
|
||||||
stringResource(R.string.interface_), padding = screenPadding)
|
) {
|
||||||
ConfigurationTextBox(
|
SectionTitle(
|
||||||
value = uiState.tunnelName,
|
stringResource(R.string.interface_),
|
||||||
onValueChange = { value -> viewModel.onTunnelNameChange(value) },
|
padding = screenPadding,
|
||||||
keyboardActions = keyboardActions,
|
)
|
||||||
label = stringResource(R.string.name),
|
ConfigurationTextBox(
|
||||||
hint = stringResource(R.string.tunnel_name).lowercase(),
|
value = uiState.tunnelName,
|
||||||
modifier =
|
onValueChange = { value -> viewModel.onTunnelNameChange(value) },
|
||||||
Modifier
|
keyboardActions = keyboardActions,
|
||||||
.fillMaxWidth()
|
label = stringResource(R.string.name),
|
||||||
.focusRequester(focusRequester))
|
hint = stringResource(R.string.tunnel_name).lowercase(),
|
||||||
OutlinedTextField(
|
modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
|
||||||
modifier =
|
)
|
||||||
Modifier.fillMaxWidth().clickable {
|
OutlinedTextField(
|
||||||
showAuthPrompt = true
|
modifier = Modifier.fillMaxWidth().clickable { showAuthPrompt = true },
|
||||||
},
|
value = uiState.interfaceProxy.privateKey,
|
||||||
value = uiState.interfaceProxy.privateKey,
|
visualTransformation =
|
||||||
visualTransformation =
|
if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated)
|
||||||
if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) ||
|
VisualTransformation.None
|
||||||
isAuthenticated)
|
else PasswordVisualTransformation(),
|
||||||
VisualTransformation.None
|
enabled = (id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
|
||||||
else PasswordVisualTransformation(),
|
onValueChange = { value -> viewModel.onPrivateKeyChange(value) },
|
||||||
enabled =
|
trailingIcon = {
|
||||||
(id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
|
IconButton(
|
||||||
onValueChange = { value -> viewModel.onPrivateKeyChange(value) },
|
modifier = Modifier.focusRequester(FocusRequester.Default),
|
||||||
trailingIcon = {
|
onClick = { viewModel.generateKeyPair() },
|
||||||
IconButton(
|
) {
|
||||||
modifier = Modifier.focusRequester(FocusRequester.Default),
|
Icon(
|
||||||
onClick = { viewModel.generateKeyPair() }) {
|
Icons.Rounded.Refresh,
|
||||||
Icon(
|
stringResource(R.string.rotate_keys),
|
||||||
Icons.Rounded.Refresh,
|
tint = Color.White,
|
||||||
stringResource(R.string.rotate_keys),
|
)
|
||||||
tint = Color.White)
|
}
|
||||||
}
|
},
|
||||||
},
|
label = { Text(stringResource(R.string.private_key)) },
|
||||||
label = { Text(stringResource(R.string.private_key)) },
|
singleLine = true,
|
||||||
singleLine = true,
|
placeholder = { Text(stringResource(R.string.base64_key)) },
|
||||||
placeholder = { Text(stringResource(R.string.base64_key)) },
|
keyboardOptions = keyboardOptions,
|
||||||
keyboardOptions = keyboardOptions,
|
keyboardActions = keyboardActions,
|
||||||
keyboardActions = keyboardActions)
|
)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.fillMaxWidth().focusRequester(FocusRequester.Default),
|
||||||
.fillMaxWidth()
|
value = uiState.interfaceProxy.publicKey,
|
||||||
.focusRequester(FocusRequester.Default),
|
enabled = false,
|
||||||
value = uiState.interfaceProxy.publicKey,
|
onValueChange = {},
|
||||||
enabled = false,
|
trailingIcon = {
|
||||||
onValueChange = {},
|
IconButton(
|
||||||
trailingIcon = {
|
modifier = Modifier.focusRequester(FocusRequester.Default),
|
||||||
IconButton(
|
onClick = {
|
||||||
modifier = Modifier.focusRequester(FocusRequester.Default),
|
clipboardManager.setText(
|
||||||
onClick = {
|
AnnotatedString(uiState.interfaceProxy.publicKey),
|
||||||
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),
|
Icon(
|
||||||
hint = stringResource(R.string.comma_separated_list),
|
Icons.Rounded.ContentCopy,
|
||||||
modifier =
|
stringResource(R.string.copy_public_key),
|
||||||
Modifier
|
tint = Color.White,
|
||||||
.fillMaxWidth(3 / 5f)
|
)
|
||||||
.padding(end = 5.dp))
|
}
|
||||||
ConfigurationTextBox(
|
},
|
||||||
value = uiState.interfaceProxy.listenPort,
|
label = { Text(stringResource(R.string.public_key)) },
|
||||||
onValueChange = { value ->
|
singleLine = true,
|
||||||
viewModel.onListenPortChanged(value)
|
placeholder = { Text(stringResource(R.string.base64_key)) },
|
||||||
},
|
keyboardOptions = keyboardOptions,
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.listen_port),
|
)
|
||||||
hint = stringResource(R.string.random),
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
modifier = Modifier.width(IntrinsicSize.Min))
|
ConfigurationTextBox(
|
||||||
}
|
value = uiState.interfaceProxy.addresses,
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
onValueChange = { value -> viewModel.onAddressesChanged(value) },
|
||||||
ConfigurationTextBox(
|
keyboardActions = keyboardActions,
|
||||||
value = uiState.interfaceProxy.dnsServers,
|
label = stringResource(R.string.addresses),
|
||||||
onValueChange = { value ->
|
hint = stringResource(R.string.comma_separated_list),
|
||||||
viewModel.onDnsServersChanged(value)
|
modifier = Modifier.fillMaxWidth(3 / 5f).padding(end = 5.dp),
|
||||||
},
|
)
|
||||||
keyboardActions = keyboardActions,
|
ConfigurationTextBox(
|
||||||
label = stringResource(R.string.dns_servers),
|
value = uiState.interfaceProxy.listenPort,
|
||||||
hint = stringResource(R.string.comma_separated_list),
|
onValueChange = { value -> viewModel.onListenPortChanged(value) },
|
||||||
modifier =
|
keyboardActions = keyboardActions,
|
||||||
Modifier
|
label = stringResource(R.string.listen_port),
|
||||||
.fillMaxWidth(3 / 5f)
|
hint = stringResource(R.string.random),
|
||||||
.padding(end = 5.dp))
|
modifier = Modifier.width(IntrinsicSize.Min),
|
||||||
ConfigurationTextBox(
|
)
|
||||||
value = uiState.interfaceProxy.mtu,
|
}
|
||||||
onValueChange = { value -> viewModel.onMtuChanged(value) },
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
keyboardActions = keyboardActions,
|
ConfigurationTextBox(
|
||||||
label = stringResource(R.string.mtu),
|
value = uiState.interfaceProxy.dnsServers,
|
||||||
hint = stringResource(R.string.auto),
|
onValueChange = { value -> viewModel.onDnsServersChanged(value) },
|
||||||
modifier = Modifier.width(IntrinsicSize.Min))
|
keyboardActions = keyboardActions,
|
||||||
}
|
label = stringResource(R.string.dns_servers),
|
||||||
Row(
|
hint = stringResource(R.string.comma_separated_list),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
modifier = Modifier.fillMaxWidth(3 / 5f).padding(end = 5.dp),
|
||||||
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
)
|
||||||
horizontalArrangement = Arrangement.Center) {
|
ConfigurationTextBox(
|
||||||
TextButton(onClick = { showApplicationsDialog = true }) {
|
value = uiState.interfaceProxy.mtu,
|
||||||
Text(applicationButtonText())
|
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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
uiState.proxyPeers.forEachIndexed { index, peer ->
|
}
|
||||||
|
}
|
||||||
|
uiState.proxyPeers.forEachIndexed { index, peer ->
|
||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
shadowElevation = 2.dp,
|
shadowElevation = 2.dp,
|
||||||
|
@ -445,106 +473,118 @@ fun ConfigScreen(
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
modifier =
|
modifier =
|
||||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Modifier.fillMaxHeight(fillMaxHeight).fillMaxWidth(fillMaxWidth)
|
Modifier.fillMaxHeight(fillMaxHeight).fillMaxWidth(fillMaxWidth)
|
||||||
} else {
|
} else {
|
||||||
Modifier.fillMaxWidth(fillMaxWidth)
|
Modifier.fillMaxWidth(fillMaxWidth)
|
||||||
})
|
})
|
||||||
.padding(top = 10.dp, bottom = 10.dp)) {
|
.padding(top = 10.dp, bottom = 10.dp),
|
||||||
Column(
|
) {
|
||||||
horizontalAlignment = Alignment.Start,
|
Column(
|
||||||
verticalArrangement = Arrangement.Top,
|
horizontalAlignment = Alignment.Start,
|
||||||
modifier =
|
verticalArrangement = Arrangement.Top,
|
||||||
Modifier.padding(horizontal = 15.dp).padding(bottom = 10.dp)) {
|
modifier = Modifier.padding(horizontal = 15.dp).padding(bottom = 10.dp),
|
||||||
Row(
|
) {
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 5.dp)) {
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
SectionTitle(
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 5.dp),
|
||||||
stringResource(R.string.peer), padding = screenPadding)
|
) {
|
||||||
IconButton(onClick = { viewModel.onDeletePeer(index) }) {
|
SectionTitle(
|
||||||
Icon(Icons.Rounded.Delete, stringResource(R.string.delete))
|
stringResource(R.string.peer),
|
||||||
}
|
padding = screenPadding,
|
||||||
}
|
)
|
||||||
|
IconButton(onClick = { viewModel.onDeletePeer(index) }) {
|
||||||
ConfigurationTextBox(
|
Icon(Icons.Rounded.Delete, stringResource(R.string.delete))
|
||||||
value = peer.publicKey,
|
}
|
||||||
onValueChange = { value ->
|
|
||||||
viewModel.onPeerPublicKeyChange(index, value)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.public_key),
|
|
||||||
hint = stringResource(R.string.base64_key),
|
|
||||||
modifier = Modifier.fillMaxWidth())
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = peer.preSharedKey,
|
|
||||||
onValueChange = { value ->
|
|
||||||
viewModel.onPreSharedKeyChange(index, value)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.preshared_key),
|
|
||||||
hint = stringResource(R.string.optional),
|
|
||||||
modifier = Modifier.fillMaxWidth())
|
|
||||||
OutlinedTextField(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
value = peer.persistentKeepalive,
|
|
||||||
enabled = true,
|
|
||||||
onValueChange = { value ->
|
|
||||||
viewModel.onPersistentKeepaliveChanged(index, value)
|
|
||||||
},
|
|
||||||
trailingIcon = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.seconds),
|
|
||||||
modifier = Modifier.padding(end = 10.dp))
|
|
||||||
},
|
|
||||||
label = { Text(stringResource(R.string.persistent_keepalive)) },
|
|
||||||
singleLine = true,
|
|
||||||
placeholder = {
|
|
||||||
Text(stringResource(R.string.optional_no_recommend))
|
|
||||||
},
|
|
||||||
keyboardOptions = keyboardOptions,
|
|
||||||
keyboardActions = keyboardActions)
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = peer.endpoint,
|
|
||||||
onValueChange = { value ->
|
|
||||||
viewModel.onEndpointChange(index, value)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.endpoint),
|
|
||||||
hint = stringResource(R.string.endpoint).lowercase(),
|
|
||||||
modifier = Modifier.fillMaxWidth())
|
|
||||||
OutlinedTextField(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
value = peer.allowedIps,
|
|
||||||
enabled = true,
|
|
||||||
onValueChange = { value ->
|
|
||||||
viewModel.onAllowedIpsChange(index, value)
|
|
||||||
},
|
|
||||||
label = { Text(stringResource(R.string.allowed_ips)) },
|
|
||||||
singleLine = true,
|
|
||||||
placeholder = {
|
|
||||||
Text(stringResource(R.string.comma_separated_list))
|
|
||||||
},
|
|
||||||
keyboardOptions = keyboardOptions,
|
|
||||||
keyboardActions = keyboardActions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxSize().padding(bottom = 140.dp)) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.Center) {
|
|
||||||
TextButton(onClick = { viewModel.addEmptyPeer() }) {
|
|
||||||
Text(stringResource(R.string.add_peer))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = peer.publicKey,
|
||||||
|
onValueChange = { value ->
|
||||||
|
viewModel.onPeerPublicKeyChange(index, value)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.public_key),
|
||||||
|
hint = stringResource(R.string.base64_key),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = peer.preSharedKey,
|
||||||
|
onValueChange = { value ->
|
||||||
|
viewModel.onPreSharedKeyChange(index, value)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.preshared_key),
|
||||||
|
hint = stringResource(R.string.optional),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = peer.persistentKeepalive,
|
||||||
|
enabled = true,
|
||||||
|
onValueChange = { value ->
|
||||||
|
viewModel.onPersistentKeepaliveChanged(index, value)
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.seconds),
|
||||||
|
modifier = Modifier.padding(end = 10.dp),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = { Text(stringResource(R.string.persistent_keepalive)) },
|
||||||
|
singleLine = true,
|
||||||
|
placeholder = {
|
||||||
|
Text(stringResource(R.string.optional_no_recommend))
|
||||||
|
},
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = peer.endpoint,
|
||||||
|
onValueChange = { value ->
|
||||||
|
viewModel.onEndpointChange(index, value)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.endpoint),
|
||||||
|
hint = stringResource(R.string.endpoint).lowercase(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = peer.allowedIps,
|
||||||
|
enabled = true,
|
||||||
|
onValueChange = { value ->
|
||||||
|
viewModel.onAllowedIpsChange(index, value)
|
||||||
|
},
|
||||||
|
label = { Text(stringResource(R.string.allowed_ips)) },
|
||||||
|
singleLine = true,
|
||||||
|
placeholder = {
|
||||||
|
Text(stringResource(R.string.comma_separated_list))
|
||||||
|
},
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxSize().padding(bottom = 140.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
TextButton(onClick = { viewModel.addEmptyPeer() }) {
|
||||||
|
Text(stringResource(R.string.add_peer))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
|
||||||
Spacer(modifier = Modifier.weight(.17f))
|
|
||||||
}
|
}
|
||||||
}
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
|
Spacer(modifier = Modifier.weight(.17f))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,8 +11,8 @@ data class ConfigUiState(
|
||||||
val packages: Packages = emptyList(),
|
val packages: Packages = emptyList(),
|
||||||
val checkedPackageNames: List<String> = emptyList(),
|
val checkedPackageNames: List<String> = emptyList(),
|
||||||
val include: Boolean = true,
|
val include: Boolean = true,
|
||||||
val isAllApplicationsEnabled : Boolean = false,
|
val isAllApplicationsEnabled: Boolean = false,
|
||||||
val loading: Boolean = true,
|
val loading: Boolean = true,
|
||||||
val tunnel: TunnelConfig? = null,
|
val tunnel: TunnelConfig? = null,
|
||||||
val tunnelName: String = ""
|
val tunnelName: String = ""
|
||||||
)
|
)
|
||||||
|
|
|
@ -41,272 +41,312 @@ constructor(
|
||||||
private val settingsRepository: SettingsRepository,
|
private val settingsRepository: SettingsRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val packageManager = application.packageManager
|
private val packageManager = application.packageManager
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(ConfigUiState())
|
private val _uiState = MutableStateFlow(ConfigUiState())
|
||||||
val uiState = _uiState.asStateFlow()
|
val uiState = _uiState.asStateFlow()
|
||||||
|
|
||||||
fun init(tunnelId : String) = viewModelScope.launch(Dispatchers.IO) {
|
fun init(tunnelId: String) =
|
||||||
val packages = getQueriedPackages("")
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val state = if(tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
val packages = getQueriedPackages("")
|
||||||
val tunnelConfig =
|
val state =
|
||||||
tunnelConfigRepository.getAll().firstOrNull { it.id.toString() == tunnelId }
|
if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
||||||
if (tunnelConfig != null) {
|
val tunnelConfig =
|
||||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
tunnelConfigRepository.getAll().firstOrNull { it.id.toString() == tunnelId }
|
||||||
val proxyPeers = config.peers.map { PeerProxy.from(it) }
|
if (tunnelConfig != null) {
|
||||||
val proxyInterface = InterfaceProxy.from(config.`interface`)
|
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||||
var include = true
|
val proxyPeers = config.peers.map { PeerProxy.from(it) }
|
||||||
var isAllApplicationsEnabled = false
|
val proxyInterface = InterfaceProxy.from(config.`interface`)
|
||||||
val checkedPackages =
|
var include = true
|
||||||
if (config.`interface`.includedApplications.isNotEmpty()) {
|
var isAllApplicationsEnabled = false
|
||||||
config.`interface`.includedApplications
|
val checkedPackages =
|
||||||
} else if (config.`interface`.excludedApplications.isNotEmpty()) {
|
if (config.`interface`.includedApplications.isNotEmpty()) {
|
||||||
include = false
|
config.`interface`.includedApplications
|
||||||
config.`interface`.excludedApplications
|
} else if (config.`interface`.excludedApplications.isNotEmpty()) {
|
||||||
|
include = false
|
||||||
|
config.`interface`.excludedApplications
|
||||||
|
} else {
|
||||||
|
isAllApplicationsEnabled = true
|
||||||
|
emptySet()
|
||||||
|
}
|
||||||
|
ConfigUiState(
|
||||||
|
proxyPeers,
|
||||||
|
proxyInterface,
|
||||||
|
packages,
|
||||||
|
checkedPackages.toList(),
|
||||||
|
include,
|
||||||
|
isAllApplicationsEnabled,
|
||||||
|
false,
|
||||||
|
tunnelConfig,
|
||||||
|
tunnelConfig.name,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
isAllApplicationsEnabled = true
|
ConfigUiState(loading = false, packages = packages)
|
||||||
emptySet()
|
|
||||||
}
|
}
|
||||||
ConfigUiState(
|
} else {
|
||||||
proxyPeers,
|
ConfigUiState(loading = false, packages = packages)
|
||||||
proxyInterface,
|
}
|
||||||
packages,
|
_uiState.value = state
|
||||||
checkedPackages.toList(),
|
}
|
||||||
include,
|
|
||||||
isAllApplicationsEnabled,
|
fun onTunnelNameChange(name: String) {
|
||||||
false,
|
_uiState.value = _uiState.value.copy(tunnelName = name)
|
||||||
tunnelConfig,
|
}
|
||||||
tunnelConfig.name)
|
|
||||||
} else {
|
fun onIncludeChange(include: Boolean) {
|
||||||
ConfigUiState(loading = false, packages = packages)
|
_uiState.value = _uiState.value.copy(include = include)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onAddCheckedPackage(packageName: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(
|
||||||
|
checkedPackageNames = _uiState.value.checkedPackageNames + packageName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAllApplicationsChange(isAllApplicationsEnabled: Boolean) {
|
||||||
|
_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 {
|
} else {
|
||||||
ConfigUiState(loading = false, packages = packages)
|
packageManager.getPackagesHoldingPermissions(permissions, 0)
|
||||||
}
|
}
|
||||||
_uiState.value = state
|
|
||||||
}
|
}
|
||||||
fun onTunnelNameChange(name: String) {
|
|
||||||
_uiState.value = _uiState.value.copy(tunnelName = name)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onIncludeChange(include: Boolean) {
|
private fun isAllApplicationsEnabled(): Boolean {
|
||||||
_uiState.value = _uiState.value.copy(include = include)
|
return _uiState.value.isAllApplicationsEnabled
|
||||||
}
|
|
||||||
|
|
||||||
fun onAddCheckedPackage(packageName: String) {
|
|
||||||
_uiState.value =
|
|
||||||
_uiState.value.copy(checkedPackageNames = _uiState.value.checkedPackageNames + packageName)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onAllApplicationsChange(isAllApplicationsEnabled: Boolean) {
|
|
||||||
_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 {
|
private fun saveConfig(tunnelConfig: TunnelConfig) =
|
||||||
return packageInfo.applicationInfo.loadLabel(application.packageManager).toString()
|
viewModelScope.launch { tunnelConfigRepository.save(tunnelConfig) }
|
||||||
}
|
|
||||||
|
|
||||||
private fun getAllInternetCapablePackages(): List<PackageInfo> {
|
private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) =
|
||||||
return getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET))
|
viewModelScope.launch {
|
||||||
}
|
if (tunnelConfig != null) {
|
||||||
|
saveConfig(tunnelConfig).join()
|
||||||
private fun getPackagesHoldingPermissions(permissions: Array<String>): List<PackageInfo> {
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
updateSettingsDefaultTunnel(tunnelConfig)
|
||||||
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) {
|
private suspend fun updateSettingsDefaultTunnel(tunnelConfig: TunnelConfig) {
|
||||||
val settings = settingsRepository.getSettingsFlow().first()
|
val settings = settingsRepository.getSettingsFlow().first()
|
||||||
if (settings.defaultTunnel != null) {
|
if (settings.defaultTunnel != null) {
|
||||||
if (tunnelConfig.id == TunnelConfig.from(settings.defaultTunnel!!).id) {
|
if (tunnelConfig.id == TunnelConfig.from(settings.defaultTunnel!!).id) {
|
||||||
settingsRepository.save(settings.copy(defaultTunnel = tunnelConfig.toString()))
|
settingsRepository.save(settings.copy(defaultTunnel = tunnelConfig.toString()))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildPeerListFromProxyPeers(): List<Peer> {
|
|
||||||
return _uiState.value.proxyPeers.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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun emptyCheckedPackagesList() {
|
|
||||||
_uiState.value = _uiState.value.copy(checkedPackageNames = emptyList())
|
|
||||||
}
|
|
||||||
|
|
||||||
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 buildPeerListFromProxyPeers(): List<Peer> {
|
||||||
|
return _uiState.value.proxyPeers.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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emptyCheckedPackagesList() {
|
||||||
|
_uiState.value = _uiState.value.copy(checkedPackageNames = emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildInterfaceListFromProxyInterface(): Interface {
|
||||||
|
val builder = Interface.Builder()
|
||||||
|
builder.parsePrivateKey(_uiState.value.interfaceProxy.privateKey.trim())
|
||||||
|
builder.parseAddresses(_uiState.value.interfaceProxy.addresses.trim())
|
||||||
|
if (_uiState.value.interfaceProxy.dnsServers.isNotEmpty()) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,6 +107,7 @@ import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||||
|
@ -118,370 +119,472 @@ fun MainScreen(
|
||||||
showSnackbarMessage: (String) -> Unit,
|
showSnackbarMessage: (String) -> Unit,
|
||||||
navController: NavController
|
navController: NavController
|
||||||
) {
|
) {
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val isVisible = rememberSaveable { mutableStateOf(true) }
|
val isVisible = rememberSaveable { mutableStateOf(true) }
|
||||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||||
|
|
||||||
val sheetState = rememberModalBottomSheetState()
|
val sheetState = rememberModalBottomSheetState()
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
var showPrimaryChangeAlertDialog by remember { mutableStateOf(false) }
|
var showPrimaryChangeAlertDialog by remember { mutableStateOf(false) }
|
||||||
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
||||||
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
LaunchedEffect(uiState.loading) {
|
LaunchedEffect(uiState.loading) {
|
||||||
if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
delay(Constants.FOCUS_REQUEST_DELAY)
|
delay(Constants.FOCUS_REQUEST_DELAY)
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (uiState.loading) {
|
if (uiState.loading) {
|
||||||
LoadingScreen()
|
LoadingScreen()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val tunnelFileImportResultLauncher =
|
val tunnelFileImportResultLauncher =
|
||||||
rememberLauncherForActivityResult(
|
rememberLauncherForActivityResult(
|
||||||
object : ActivityResultContracts.GetContent() {
|
object : ActivityResultContracts.GetContent() {
|
||||||
override fun createIntent(context: Context, input: String): Intent {
|
override fun createIntent(context: Context, input: String): Intent {
|
||||||
val intent = super.createIntent(context, input)
|
val intent = super.createIntent(context, input)
|
||||||
|
|
||||||
/* AndroidTV now comes with stubs that do nothing but display a Toast less helpful than
|
/* 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. */
|
* what we can do, so detect this and throw an exception that we can catch later. */
|
||||||
val activitiesToResolveIntent =
|
val activitiesToResolveIntent =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
context.packageManager.queryIntentActivities(
|
context.packageManager.queryIntentActivities(
|
||||||
intent,
|
intent,
|
||||||
PackageManager.ResolveInfoFlags.of(
|
PackageManager.ResolveInfoFlags.of(
|
||||||
PackageManager.MATCH_DEFAULT_ONLY.toLong()))
|
PackageManager.MATCH_DEFAULT_ONLY.toLong(),
|
||||||
} else {
|
),
|
||||||
context.packageManager.queryIntentActivities(
|
)
|
||||||
intent, PackageManager.MATCH_DEFAULT_ONLY)
|
} else {
|
||||||
}
|
context.packageManager.queryIntentActivities(
|
||||||
if (activitiesToResolveIntent.all {
|
intent,
|
||||||
val name = it.activityInfo.packageName
|
PackageManager.MATCH_DEFAULT_ONLY,
|
||||||
name.startsWith(Constants.GOOGLE_TV_EXPLORER_STUB) ||
|
)
|
||||||
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
|
}
|
||||||
}) {
|
if (
|
||||||
showSnackbarMessage(Event.Error.FileExplorerRequired.message)
|
activitiesToResolveIntent.all {
|
||||||
}
|
val name = it.activityInfo.packageName
|
||||||
return intent
|
name.startsWith(Constants.GOOGLE_TV_EXPLORER_STUB) ||
|
||||||
}
|
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
|
||||||
}) { data ->
|
}
|
||||||
|
) {
|
||||||
|
showSnackbarMessage(Event.Error.FileExplorerRequired.message)
|
||||||
|
}
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) { data ->
|
||||||
if (data == null) return@rememberLauncherForActivityResult
|
if (data == null) return@rememberLauncherForActivityResult
|
||||||
scope.launch {
|
scope.launch {
|
||||||
viewModel.onTunnelFileSelected(data).let {
|
viewModel.onTunnelFileSelected(data).let {
|
||||||
when (it) {
|
when (it) {
|
||||||
is Result.Error -> showSnackbarMessage(it.error.message)
|
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||||
is Result.Success -> {}
|
is Result.Success -> {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val scanLauncher =
|
val scanLauncher =
|
||||||
rememberLauncherForActivityResult(
|
rememberLauncherForActivityResult(
|
||||||
contract = ScanContract(),
|
contract = ScanContract(),
|
||||||
onResult = {
|
onResult = {
|
||||||
if (it.contents != null) {
|
if (it.contents != null) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
viewModel.onTunnelQrResult(it.contents).let { result ->
|
viewModel.onTunnelQrResult(it.contents).let { result ->
|
||||||
when (result) {
|
when (result) {
|
||||||
is Result.Success -> {}
|
is Result.Success -> {}
|
||||||
is Result.Error -> showSnackbarMessage(result.error.message)
|
is Result.Error -> showSnackbarMessage(result.error.message)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
AnimatedVisibility(showPrimaryChangeAlertDialog) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { showPrimaryChangeAlertDialog = false },
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
viewModel.onDefaultTunnelChange(selectedTunnel)
|
|
||||||
showPrimaryChangeAlertDialog = false
|
|
||||||
selectedTunnel = null
|
|
||||||
}) {
|
|
||||||
Text(text = stringResource(R.string.okay))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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
|
|
||||||
})
|
|
||||||
},
|
|
||||||
floatingActionButtonPosition = FabPosition.End,
|
|
||||||
topBar = {
|
|
||||||
if (uiState.settings.isAutoTunnelEnabled)
|
|
||||||
TopAppBar(
|
|
||||||
title = {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.requiredWidth(LocalConfiguration.current.screenWidthDp.dp).padding(end = 5.dp)) {
|
|
||||||
Row {
|
|
||||||
Icon(
|
|
||||||
Icons.Rounded.Bolt,
|
|
||||||
stringResource(id = R.string.auto),
|
|
||||||
modifier = Modifier.size(25.dp),
|
|
||||||
tint = if(uiState.settings.isAutoTunnelPaused) Color.Gray else mint)
|
|
||||||
Text(
|
|
||||||
"Auto-tunneling: ${if(uiState.settings.isAutoTunnelPaused) "paused" else "active" }",
|
|
||||||
style = typography.bodyLarge,
|
|
||||||
modifier = Modifier.padding(start = 10.dp))
|
|
||||||
}
|
|
||||||
if(uiState.settings.isAutoTunnelPaused) TextButton(
|
|
||||||
onClick = { viewModel.resumeAutoTunneling() },
|
|
||||||
modifier = Modifier.padding(end = 10.dp)) {
|
|
||||||
Text("Resume")
|
|
||||||
} else TextButton(
|
|
||||||
onClick = { viewModel.pauseAutoTunneling() },
|
|
||||||
modifier = Modifier.padding(end = 10.dp)) {
|
|
||||||
Text("Pause")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
floatingActionButton = {
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = isVisible.value,
|
|
||||||
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
|
||||||
exit = slideOutVertically(targetOffsetY = { it * 2 })) {
|
|
||||||
val secondaryColor = MaterialTheme.colorScheme.secondary
|
|
||||||
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
|
||||||
var fobColor by remember { mutableStateOf(secondaryColor) }
|
|
||||||
FloatingActionButton(
|
|
||||||
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,
|
}
|
||||||
shape = RoundedCornerShape(16.dp)) {
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(showPrimaryChangeAlertDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showPrimaryChangeAlertDialog = false },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.onDefaultTunnelChange(selectedTunnel)
|
||||||
|
showPrimaryChangeAlertDialog = false
|
||||||
|
selectedTunnel = null
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.okay))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(showDeleteTunnelAlertDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showDeleteTunnelAlertDialog = false },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
selectedTunnel?.let { viewModel.onDelete(it) }
|
||||||
|
showDeleteTunnelAlertDialog = false
|
||||||
|
selectedTunnel = null
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.yes))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showDeleteTunnelAlertDialog = false }) {
|
||||||
|
Text(text = stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = { Text(text = stringResource(R.string.delete_tunnel)) },
|
||||||
|
text = { Text(text = stringResource(R.string.delete_tunnel_message)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
floatingActionButtonPosition = FabPosition.End,
|
||||||
|
topBar = {
|
||||||
|
if (uiState.settings.isAutoTunnelEnabled)
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier =
|
||||||
|
Modifier.requiredWidth(LocalConfiguration.current.screenWidthDp.dp)
|
||||||
|
.padding(end = 5.dp),
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Bolt,
|
||||||
|
stringResource(id = R.string.auto),
|
||||||
|
modifier = Modifier.size(25.dp),
|
||||||
|
tint =
|
||||||
|
if (uiState.settings.isAutoTunnelPaused) Color.Gray
|
||||||
|
else mint,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"Auto-tunneling: ${if (uiState.settings.isAutoTunnelPaused) "paused" else "active"}",
|
||||||
|
style = typography.bodyLarge,
|
||||||
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (uiState.settings.isAutoTunnelPaused)
|
||||||
|
TextButton(
|
||||||
|
onClick = { viewModel.resumeAutoTunneling() },
|
||||||
|
modifier = Modifier.padding(end = 10.dp),
|
||||||
|
) {
|
||||||
|
Text("Resume")
|
||||||
|
}
|
||||||
|
else
|
||||||
|
TextButton(
|
||||||
|
onClick = { viewModel.pauseAutoTunneling() },
|
||||||
|
modifier = Modifier.padding(end = 10.dp),
|
||||||
|
) {
|
||||||
|
Text("Pause")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isVisible.value,
|
||||||
|
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
||||||
|
exit = slideOutVertically(targetOffsetY = { it * 2 }),
|
||||||
|
) {
|
||||||
|
val secondaryColor = MaterialTheme.colorScheme.secondary
|
||||||
|
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||||
|
var fobColor by remember { mutableStateOf(secondaryColor) }
|
||||||
|
FloatingActionButton(
|
||||||
|
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,
|
||||||
|
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 ->
|
},
|
||||||
|
) { innerPadding ->
|
||||||
AnimatedVisibility(uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) {
|
AnimatedVisibility(uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
modifier = Modifier.fillMaxSize().padding(padding)) {
|
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.fillMaxWidth()
|
Modifier.fillMaxWidth()
|
||||||
.clickable {
|
.clickable {
|
||||||
showBottomSheet = false
|
showBottomSheet = false
|
||||||
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
||||||
}
|
}
|
||||||
.padding(10.dp)) {
|
.padding(10.dp),
|
||||||
Icon(
|
) {
|
||||||
Icons.Filled.FileOpen,
|
Icon(
|
||||||
contentDescription = stringResource(id = R.string.open_file),
|
Icons.Filled.FileOpen,
|
||||||
modifier = Modifier.padding(10.dp))
|
contentDescription = stringResource(id = R.string.open_file),
|
||||||
Text(
|
modifier = Modifier.padding(10.dp),
|
||||||
stringResource(id = R.string.add_tunnels_text),
|
)
|
||||||
modifier = Modifier.padding(10.dp))
|
Text(
|
||||||
}
|
stringResource(id = R.string.add_tunnels_text),
|
||||||
|
modifier = Modifier.padding(10.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Divider()
|
Divider()
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth()
|
Modifier.fillMaxWidth()
|
||||||
.clickable {
|
.clickable {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
showBottomSheet = false
|
showBottomSheet = false
|
||||||
val scanOptions = ScanOptions()
|
val scanOptions = ScanOptions()
|
||||||
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
||||||
scanOptions.setOrientationLocked(true)
|
scanOptions.setOrientationLocked(true)
|
||||||
scanOptions.setPrompt(context.getString(R.string.scanning_qr))
|
scanOptions.setPrompt(
|
||||||
scanOptions.setBeepEnabled(false)
|
context.getString(R.string.scanning_qr)
|
||||||
scanOptions.captureActivity = CaptureActivityPortrait::class.java
|
)
|
||||||
scanLauncher.launch(scanOptions)
|
scanOptions.setBeepEnabled(false)
|
||||||
|
scanOptions.captureActivity =
|
||||||
|
CaptureActivityPortrait::class.java
|
||||||
|
scanLauncher.launch(scanOptions)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
.padding(10.dp),
|
||||||
.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.fillMaxWidth()
|
Modifier.fillMaxWidth()
|
||||||
.clickable {
|
.clickable {
|
||||||
showBottomSheet = false
|
showBottomSheet = false
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}")
|
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.padding(10.dp)) {
|
.padding(10.dp),
|
||||||
Icon(
|
) {
|
||||||
Icons.Filled.Create,
|
Icon(
|
||||||
contentDescription = stringResource(id = R.string.create_import),
|
Icons.Filled.Create,
|
||||||
modifier = Modifier.padding(10.dp))
|
contentDescription = stringResource(id = R.string.create_import),
|
||||||
Text(
|
modifier = Modifier.padding(10.dp),
|
||||||
stringResource(id = R.string.create_import),
|
)
|
||||||
modifier = Modifier.padding(10.dp))
|
Text(
|
||||||
}
|
stringResource(id = R.string.create_import),
|
||||||
}
|
modifier = Modifier.padding(10.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth().fillMaxHeight(.90f).overscroll(ScrollableDefaults.overscrollEffect()).padding(innerPadding),
|
Modifier.fillMaxWidth()
|
||||||
|
.fillMaxHeight(.90f)
|
||||||
|
.overscroll(ScrollableDefaults.overscrollEffect())
|
||||||
|
.padding(innerPadding),
|
||||||
state = rememberLazyListState(0, uiState.tunnels.count()),
|
state = rememberLazyListState(0, uiState.tunnels.count()),
|
||||||
userScrollEnabled = true,
|
userScrollEnabled = true,
|
||||||
reverseLayout = true,
|
reverseLayout = true,
|
||||||
flingBehavior = ScrollableDefaults.flingBehavior()) {
|
flingBehavior = ScrollableDefaults.flingBehavior(),
|
||||||
items(uiState.tunnels,
|
) {
|
||||||
key = { tunnel -> tunnel.id }) { tunnel ->
|
items(
|
||||||
val leadingIconColor =
|
uiState.tunnels,
|
||||||
(if (uiState.vpnState.name == tunnel.name &&
|
key = { tunnel -> tunnel.id },
|
||||||
uiState.vpnState.status == Tunnel.State.UP) {
|
) { tunnel ->
|
||||||
uiState.vpnState.statistics
|
val leadingIconColor =
|
||||||
?.mapPeerStats()
|
(if (
|
||||||
?.map { it.value?.handshakeStatus() }
|
uiState.vpnState.name == tunnel.name &&
|
||||||
.let { statuses ->
|
uiState.vpnState.status == Tunnel.State.UP
|
||||||
|
) {
|
||||||
|
uiState.vpnState.statistics
|
||||||
|
?.mapPeerStats()
|
||||||
|
?.map { it.value?.handshakeStatus() }
|
||||||
|
.let { statuses ->
|
||||||
when {
|
when {
|
||||||
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> mint
|
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> mint
|
||||||
statuses?.any { it == HandshakeStatus.STALE } == true -> corn
|
statuses?.any { it == HandshakeStatus.STALE } == true -> corn
|
||||||
statuses?.all { it == HandshakeStatus.NOT_STARTED } == true ->
|
statuses?.all { it == HandshakeStatus.NOT_STARTED } == true ->
|
||||||
Color.Gray
|
Color.Gray
|
||||||
else -> {
|
else -> {
|
||||||
Color.Gray
|
Color.Gray
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Color.Gray
|
Color.Gray
|
||||||
})
|
})
|
||||||
val expanded = remember { mutableStateOf(false) }
|
val expanded = remember { mutableStateOf(false) }
|
||||||
RowListItem(
|
RowListItem(
|
||||||
icon = {
|
icon = {
|
||||||
if (uiState.settings.isTunnelConfigDefault(tunnel)) {
|
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.padding(end = 10.dp).size(20.dp),
|
||||||
} else {
|
)
|
||||||
|
} else {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Circle,
|
Icons.Rounded.Circle,
|
||||||
stringResource(R.string.status),
|
stringResource(R.string.status),
|
||||||
tint = leadingIconColor,
|
tint = leadingIconColor,
|
||||||
modifier = Modifier.padding(end = 15.dp).size(15.dp))
|
modifier = Modifier.padding(end = 15.dp).size(15.dp),
|
||||||
}
|
)
|
||||||
},
|
}
|
||||||
text = tunnel.name,
|
},
|
||||||
onHold = {
|
text = tunnel.name,
|
||||||
if ((uiState.vpnState.status == Tunnel.State.UP) &&
|
onHold = {
|
||||||
(tunnel.name == uiState.vpnState.name)) {
|
if (
|
||||||
|
(uiState.vpnState.status == Tunnel.State.UP) &&
|
||||||
|
(tunnel.name == uiState.vpnState.name)
|
||||||
|
) {
|
||||||
showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
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()) {
|
||||||
if (uiState.vpnState.status == Tunnel.State.UP &&
|
if (
|
||||||
(uiState.vpnState.name == tunnel.name)) {
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
expanded.value = !expanded.value
|
(uiState.vpnState.name == tunnel.name)
|
||||||
|
) {
|
||||||
|
expanded.value = !expanded.value
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
selectedTunnel = tunnel
|
selectedTunnel = tunnel
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
statistics = uiState.vpnState.statistics,
|
statistics = uiState.vpnState.statistics,
|
||||||
expanded = expanded.value,
|
expanded = expanded.value,
|
||||||
rowButton = {
|
rowButton = {
|
||||||
if (tunnel.id == selectedTunnel?.id &&
|
if (
|
||||||
!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
tunnel.id == selectedTunnel?.id &&
|
||||||
|
!WireGuardAutoTunnel.isRunningOnAndroidTv()
|
||||||
|
) {
|
||||||
Row {
|
Row {
|
||||||
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
|
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (
|
||||||
|
uiState.settings.isAutoTunnelEnabled &&
|
||||||
|
!uiState.settings.isAutoTunnelPaused
|
||||||
|
) {
|
||||||
|
showSnackbarMessage(
|
||||||
|
Event.Message.AutoTunnelOffAction.message,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
showPrimaryChangeAlertDialog = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Star,
|
||||||
|
stringResource(id = R.string.set_primary),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (uiState.settings.isAutoTunnelEnabled && !uiState.settings.isAutoTunnelPaused) {
|
if (
|
||||||
showSnackbarMessage(
|
uiState.settings.isAutoTunnelEnabled &&
|
||||||
Event.Message.AutoTunnelOffAction.message)
|
uiState.settings.isTunnelConfigDefault(
|
||||||
} else {
|
tunnel,
|
||||||
showPrimaryChangeAlertDialog = true
|
) &&
|
||||||
}
|
!uiState.settings.isAutoTunnelPaused
|
||||||
}) {
|
) {
|
||||||
Icon(
|
showSnackbarMessage(
|
||||||
Icons.Rounded.Star,
|
Event.Message.AutoTunnelOffAction.message,
|
||||||
stringResource(id = R.string.set_primary))
|
)
|
||||||
}
|
} else
|
||||||
}
|
navController.navigate(
|
||||||
IconButton(
|
"${Screen.Config.route}/${selectedTunnel?.id}",
|
||||||
onClick = {
|
)
|
||||||
if (uiState.settings.isAutoTunnelEnabled && uiState.settings.isTunnelConfigDefault(tunnel)
|
},
|
||||||
&& !uiState.settings.isAutoTunnelPaused) {
|
) {
|
||||||
showSnackbarMessage(
|
|
||||||
Event.Message.AutoTunnelOffAction.message)
|
|
||||||
} else navController.navigate(
|
|
||||||
"${Screen.Config.route}/${selectedTunnel?.id}")
|
|
||||||
}) {
|
|
||||||
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
|
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.focusable(),
|
modifier = Modifier.focusable(),
|
||||||
onClick = { viewModel.onDelete(tunnel) }) {
|
onClick = { showDeleteTunnelAlertDialog = true },
|
||||||
|
) {
|
||||||
Icon(Icons.Rounded.Delete, stringResource(id = R.string.delete))
|
Icon(Icons.Rounded.Delete, stringResource(id = R.string.delete))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val checked by remember {
|
val checked by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
(uiState.vpnState.status == Tunnel.State.UP &&
|
(uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
tunnel.name == uiState.vpnState.name)
|
tunnel.name == uiState.vpnState.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!checked) expanded.value = false
|
if (!checked) expanded.value = false
|
||||||
|
|
||||||
|
@ -491,72 +594,94 @@ fun MainScreen(
|
||||||
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()) {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Row {
|
Row {
|
||||||
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
|
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (uiState.settings.isAutoTunnelEnabled) {
|
if (uiState.settings.isAutoTunnelEnabled) {
|
||||||
showSnackbarMessage(
|
showSnackbarMessage(
|
||||||
Event.Message.AutoTunnelOffAction.message)
|
Event.Message.AutoTunnelOffAction.message,
|
||||||
} else {
|
)
|
||||||
selectedTunnel = tunnel
|
} else {
|
||||||
showPrimaryChangeAlertDialog = true
|
selectedTunnel = tunnel
|
||||||
|
showPrimaryChangeAlertDialog = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Star,
|
||||||
|
stringResource(id = R.string.set_primary),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}) {
|
}
|
||||||
|
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 {
|
||||||
|
showDeleteTunnelAlertDialog = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Star,
|
Icons.Rounded.Delete,
|
||||||
stringResource(id = R.string.set_primary))
|
stringResource(id = R.string.delete),
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
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 {
|
} else {
|
||||||
TunnelSwitch()
|
TunnelSwitch()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,8 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
||||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||||
|
|
||||||
data class MainUiState(
|
data class MainUiState(
|
||||||
val settings : Settings = Settings(),
|
val settings: Settings = Settings(),
|
||||||
val tunnels : TunnelConfigs = emptyList(),
|
val tunnels: TunnelConfigs = emptyList(),
|
||||||
val vpnState: VpnState = VpnState(),
|
val vpnState: VpnState = VpnState(),
|
||||||
val loading : Boolean = true
|
val loading: Boolean = true
|
||||||
)
|
)
|
||||||
|
|
|
@ -14,11 +14,7 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
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.foreground.ServiceState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
|
|
||||||
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.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
|
@ -32,6 +28,7 @@ import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import timber.log.Timber
|
||||||
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
|
||||||
|
@ -46,216 +43,225 @@ constructor(
|
||||||
private val vpnService: VpnService
|
private val vpnService: VpnService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val uiState =
|
val uiState =
|
||||||
combine(
|
combine(
|
||||||
settingsRepository.getSettingsFlow(),
|
settingsRepository.getSettingsFlow(),
|
||||||
tunnelConfigRepository.getTunnelConfigsFlow(),
|
tunnelConfigRepository.getTunnelConfigsFlow(),
|
||||||
vpnService.vpnState,
|
vpnService.vpnState,
|
||||||
) { settings, tunnels, vpnState ->
|
) { settings, tunnels, vpnState ->
|
||||||
validateWatcherServiceState(settings)
|
validateWatcherServiceState(settings)
|
||||||
MainUiState(settings, tunnels, vpnState, false)
|
MainUiState(settings, tunnels, vpnState, false)
|
||||||
}
|
}
|
||||||
.stateIn(
|
.stateIn(
|
||||||
viewModelScope,
|
viewModelScope,
|
||||||
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
||||||
MainUiState())
|
MainUiState(),
|
||||||
|
)
|
||||||
|
|
||||||
private fun validateWatcherServiceState(settings: Settings) = viewModelScope.launch(Dispatchers.IO) {
|
private fun validateWatcherServiceState(settings: Settings) =
|
||||||
val watcherState =
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
ServiceManager.getServiceState(
|
if (settings.isAutoTunnelEnabled) {
|
||||||
application.applicationContext, WireGuardConnectivityWatcherService::class.java)
|
ServiceManager.startWatcherService(application.applicationContext)
|
||||||
if (settings.isAutoTunnelEnabled &&
|
}
|
||||||
watcherState == ServiceState.STOPPED) {
|
|
||||||
ServiceManager.startWatcherService(application.applicationContext)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopWatcherService() = viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
ServiceManager.stopWatcherService(application.applicationContext)
|
|
||||||
}
|
|
||||||
fun onDelete(tunnel: TunnelConfig) {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
if (tunnelConfigRepository.count() == 1) {
|
|
||||||
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 stopWatcherService() =
|
||||||
ServiceManager.stopVpnService(application.applicationContext)
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
}
|
ServiceManager.stopWatcherService(application.applicationContext)
|
||||||
|
}
|
||||||
|
|
||||||
private fun validateConfigString(config: String) {
|
fun onDelete(tunnel: TunnelConfig) {
|
||||||
TunnelConfig.configFromQuick(config)
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
}
|
if (tunnelConfigRepository.count() == 1) {
|
||||||
|
stopWatcherService()
|
||||||
|
val settings = settingsRepository.getSettings()
|
||||||
|
settings.defaultTunnel = null
|
||||||
|
settings.isAutoTunnelEnabled = false
|
||||||
|
settings.isAlwaysOnVpnEnabled = false
|
||||||
|
saveSettings(settings)
|
||||||
|
}
|
||||||
|
tunnelConfigRepository.delete(tunnel)
|
||||||
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun onTunnelQrResult(result: String) : Result<Unit> {
|
fun onTunnelStart(tunnelConfig: TunnelConfig) =
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
Timber.d("On start called!")
|
||||||
|
stopActiveTunnel().await()
|
||||||
|
startTunnel(tunnelConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startTunnel(tunnelConfig: TunnelConfig) =
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
Timber.d("Start tunnel via manager")
|
||||||
|
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopActiveTunnel() =
|
||||||
|
viewModelScope.async(Dispatchers.IO) {
|
||||||
|
onTunnelStop()
|
||||||
|
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onTunnelStop() =
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
Timber.d("Stopping active tunnel")
|
||||||
|
ServiceManager.stopVpnService(application.applicationContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateConfigString(config: String) {
|
||||||
|
TunnelConfig.configFromQuick(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun onTunnelQrResult(result: String): Result<Unit> {
|
||||||
return try {
|
return try {
|
||||||
validateConfigString(result)
|
validateConfigString(result)
|
||||||
val tunnelConfig =
|
val tunnelConfig =
|
||||||
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
|
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
|
||||||
addTunnel(tunnelConfig)
|
addTunnel(tunnelConfig)
|
||||||
Result.Success(Unit)
|
Result.Success(Unit)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.Error(Event.Error.InvalidQrCode)
|
Result.Error(Event.Error.InvalidQrCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) {
|
private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) {
|
||||||
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
|
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
|
||||||
val config = Config.parse(bufferReader)
|
val config = Config.parse(bufferReader)
|
||||||
val tunnelName = getNameFromFileName(fileName)
|
val tunnelName = getNameFromFileName(fileName)
|
||||||
addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
|
addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
|
||||||
withContext(Dispatchers.IO) { stream.close() }
|
withContext(Dispatchers.IO) { stream.close() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getInputStreamFromUri(uri: Uri): InputStream? {
|
private fun getInputStreamFromUri(uri: Uri): InputStream? {
|
||||||
return application.applicationContext.contentResolver.openInputStream(uri)
|
return application.applicationContext.contentResolver.openInputStream(uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onTunnelFileSelected(uri: Uri) : Result<Unit> {
|
suspend fun onTunnelFileSelected(uri: Uri): Result<Unit> {
|
||||||
try {
|
try {
|
||||||
if(isValidUriContentScheme(uri)){
|
if (isValidUriContentScheme(uri)) {
|
||||||
val fileName = getFileName(application.applicationContext, uri)
|
val fileName = getFileName(application.applicationContext, uri)
|
||||||
when (getFileExtensionFromFileName(fileName)) {
|
when (getFileExtensionFromFileName(fileName)) {
|
||||||
Constants.CONF_FILE_EXTENSION -> saveTunnelFromConfUri(fileName, uri).let {
|
Constants.CONF_FILE_EXTENSION ->
|
||||||
when(it) {
|
saveTunnelFromConfUri(fileName, uri).let {
|
||||||
is Result.Error -> return Result.Error(Event.Error.FileReadFailed)
|
when (it) {
|
||||||
is Result.Success -> return it
|
is Result.Error -> return Result.Error(Event.Error.FileReadFailed)
|
||||||
|
is Result.Success -> return it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
|
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
|
||||||
else -> return Result.Error(Event.Error.InvalidFileExtension)
|
else -> return Result.Error(Event.Error.InvalidFileExtension)
|
||||||
}
|
}
|
||||||
return Result.Success(Unit)
|
return Result.Success(Unit)
|
||||||
} else {
|
} else {
|
||||||
return Result.Error(Event.Error.InvalidFileExtension)
|
return Result.Error(Event.Error.InvalidFileExtension)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return Result.Error(Event.Error.FileReadFailed)
|
return Result.Error(Event.Error.FileReadFailed)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun saveTunnelsFromZipUri(uri: Uri) {
|
|
||||||
ZipInputStream(getInputStreamFromUri(uri)).use { zip ->
|
|
||||||
generateSequence { zip.nextEntry }
|
|
||||||
.filterNot {
|
|
||||||
it.isDirectory || getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
|
|
||||||
}
|
|
||||||
.forEach {
|
|
||||||
val name = getNameFromFileName(it.name)
|
|
||||||
val config = Config.parse(zip)
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
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 saveTunnelsFromZipUri(uri: Uri) {
|
||||||
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
|
ZipInputStream(getInputStreamFromUri(uri)).use { zip ->
|
||||||
|
generateSequence { zip.nextEntry }
|
||||||
|
.filterNot {
|
||||||
|
it.isDirectory ||
|
||||||
|
getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
|
||||||
|
}
|
||||||
|
.forEach {
|
||||||
|
val name = getNameFromFileName(it.name)
|
||||||
|
val config = Config.parse(zip)
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
addTunnel(TunnelConfig(name = name, wgQuick = config.toWgQuickString()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
|
private suspend fun saveTunnelFromConfUri(name: String, uri: Uri): Result<Unit> {
|
||||||
tunnelConfigRepository.save(tunnelConfig)
|
val stream = getInputStreamFromUri(uri)
|
||||||
}
|
return if (stream != null) {
|
||||||
|
saveTunnelConfigFromStream(stream, name)
|
||||||
private fun getFileNameByCursor(context: Context, uri: Uri): String? {
|
Result.Success(Unit)
|
||||||
context.contentResolver.query(uri, null, null, null, null)?.use {
|
} else {
|
||||||
return getDisplayNameByCursor(it)
|
Result.Error(Event.Error.FileReadFailed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getDisplayNameColumnIndex(cursor: Cursor): Int? {
|
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
||||||
val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
saveTunnel(tunnelConfig)
|
||||||
return if (columnIndex != -1) {
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
return columnIndex
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun getDisplayNameByCursor(cursor: Cursor): String? {
|
fun pauseAutoTunneling() =
|
||||||
return if (cursor.moveToFirst()) {
|
viewModelScope.launch {
|
||||||
val index = getDisplayNameColumnIndex(cursor)
|
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
|
||||||
if (index != null) {
|
}
|
||||||
cursor.getString(index)
|
|
||||||
} else null
|
|
||||||
} else null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isValidUriContentScheme(uri: Uri): Boolean {
|
fun resumeAutoTunneling() =
|
||||||
return uri.scheme == Constants.URI_CONTENT_SCHEME
|
viewModelScope.launch {
|
||||||
}
|
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
|
||||||
private fun getFileName(context: Context, uri: Uri): String {
|
}
|
||||||
|
|
||||||
|
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
|
||||||
|
tunnelConfigRepository.save(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? {
|
||||||
|
val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||||
|
return if (columnIndex != -1) {
|
||||||
|
return columnIndex
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDisplayNameByCursor(cursor: Cursor): String? {
|
||||||
|
return if (cursor.moveToFirst()) {
|
||||||
|
val index = getDisplayNameColumnIndex(cursor)
|
||||||
|
if (index != null) {
|
||||||
|
cursor.getString(index)
|
||||||
|
} else null
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
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 getNameFromFileName(fileName: String): String {
|
||||||
viewModelScope.launch(Dispatchers.IO) { settingsRepository.save(settings) }
|
return fileName.substring(0, fileName.lastIndexOf('.'))
|
||||||
|
|
||||||
fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) = viewModelScope.launch {
|
|
||||||
if (selectedTunnel != null) {
|
|
||||||
saveSettings(uiState.value.settings.copy(defaultTunnel = selectedTunnel.toString())).join()
|
|
||||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private fun getFileExtensionFromFileName(fileName: String): String {
|
||||||
|
return try {
|
||||||
|
fileName.substring(fileName.lastIndexOf('.'))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveSettings(settings: Settings) =
|
||||||
|
viewModelScope.launch(Dispatchers.IO) { settingsRepository.save(settings) }
|
||||||
|
|
||||||
|
fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) =
|
||||||
|
viewModelScope.launch {
|
||||||
|
if (selectedTunnel != null) {
|
||||||
|
saveSettings(uiState.value.settings.copy(defaultTunnel = selectedTunnel.toString()))
|
||||||
|
.join()
|
||||||
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,17 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context.POWER_SERVICE
|
||||||
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.os.PowerManager
|
||||||
|
import android.provider.Settings
|
||||||
import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
|
import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
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
|
||||||
|
@ -83,7 +90,8 @@ import java.io.File
|
||||||
|
|
||||||
@OptIn(
|
@OptIn(
|
||||||
ExperimentalPermissionsApi::class,
|
ExperimentalPermissionsApi::class,
|
||||||
ExperimentalLayoutApi::class)
|
ExperimentalLayoutApi::class,
|
||||||
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
viewModel: SettingsViewModel = hiltViewModel(),
|
viewModel: SettingsViewModel = hiltViewModel(),
|
||||||
|
@ -91,92 +99,127 @@ fun SettingsScreen(
|
||||||
showSnackbarMessage: (String) -> Unit,
|
showSnackbarMessage: (String) -> Unit,
|
||||||
focusRequester: FocusRequester
|
focusRequester: FocusRequester
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
var currentText by remember { mutableStateOf("") }
|
var currentText by remember { mutableStateOf("") }
|
||||||
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
|
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
|
||||||
var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
|
var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
|
||||||
var didExportFiles by remember { mutableStateOf(false) }
|
var didExportFiles by remember { mutableStateOf(false) }
|
||||||
var showAuthPrompt by remember { mutableStateOf(false) }
|
var showAuthPrompt by remember { mutableStateOf(false) }
|
||||||
val focusRequester2 = remember { FocusRequester() }
|
val focusRequester2 = remember { FocusRequester() }
|
||||||
|
|
||||||
val screenPadding = 5.dp
|
val screenPadding = 5.dp
|
||||||
val fillMaxWidth = .85f
|
val fillMaxWidth = .85f
|
||||||
|
|
||||||
if (uiState.loading) {
|
if (uiState.loading) {
|
||||||
LoadingScreen()
|
LoadingScreen()
|
||||||
return
|
return
|
||||||
}
|
|
||||||
|
|
||||||
fun exportAllConfigs() {
|
|
||||||
try {
|
|
||||||
val files = uiState.tunnels.map { File(context.cacheDir, "${it.name}.conf") }
|
|
||||||
files.forEachIndexed { index, file ->
|
|
||||||
file.outputStream().use { it.write(uiState.tunnels[index].wgQuick.toByteArray()) }
|
|
||||||
}
|
|
||||||
FileUtils.saveFilesToZip(context, files)
|
|
||||||
didExportFiles = true
|
|
||||||
showSnackbarMessage(Event.Message.ConfigsExported.message)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showSnackbarMessage(Event.Error.Exception(e).message)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun saveTrustedSSID() {
|
val startForResult =
|
||||||
if (currentText.isNotEmpty()) {
|
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
viewModel.onSaveTrustedSSID(currentText).let {
|
result: ActivityResult ->
|
||||||
when(it) {
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
is Result.Success -> currentText = ""
|
val intent = result.data
|
||||||
is Result.Error -> showSnackbarMessage(it.error.message)
|
// Handle the Intent
|
||||||
}
|
}
|
||||||
}
|
viewModel.setBatteryOptimizeDisableShown()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openSettings() {
|
|
||||||
scope.launch {
|
|
||||||
val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
|
|
||||||
intentSettings.data = Uri.fromParts("package", context.packageName, null)
|
|
||||||
context.startActivity(intentSettings)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkFineLocationGranted() {
|
|
||||||
isBackgroundLocationGranted =
|
|
||||||
if (!fineLocationState.status.isGranted) {
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
viewModel.setLocationDisclosureShown()
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
fun exportAllConfigs() {
|
||||||
if(WireGuardAutoTunnel.isRunningOnAndroidTv() && Build.VERSION.SDK_INT == Build.VERSION_CODES.Q){
|
try {
|
||||||
checkFineLocationGranted()
|
val files = uiState.tunnels.map { File(context.cacheDir, "${it.name}.conf") }
|
||||||
} else {
|
files.forEachIndexed { index, file ->
|
||||||
val backgroundLocationState =
|
file.outputStream().use { it.write(uiState.tunnels[index].wgQuick.toByteArray()) }
|
||||||
rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
}
|
||||||
isBackgroundLocationGranted =
|
FileUtils.saveFilesToZip(context, files)
|
||||||
if (!backgroundLocationState.status.isGranted) {
|
didExportFiles = true
|
||||||
false
|
showSnackbarMessage(Event.Message.ConfigsExported.message)
|
||||||
} else {
|
} catch (e: Exception) {
|
||||||
SideEffect { viewModel.setLocationDisclosureShown() }
|
showSnackbarMessage(Event.Error.Exception(e).message)
|
||||||
true
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
|
fun isBatteryOptimizationsDisabled(): Boolean {
|
||||||
checkFineLocationGranted()
|
val pm = context.getSystemService(POWER_SERVICE) as PowerManager
|
||||||
}
|
return pm.isIgnoringBatteryOptimizations(context.packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestBatteryOptimizationsDisabled() {
|
||||||
|
val intent =
|
||||||
|
Intent().apply {
|
||||||
|
this.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
||||||
|
data = Uri.fromParts("package", context.packageName, null)
|
||||||
|
}
|
||||||
|
startForResult.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleAutoTunnelToggle() {
|
||||||
|
if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) {
|
||||||
|
viewModel.toggleAutoTunnel()
|
||||||
|
} else {
|
||||||
|
requestBatteryOptimizationsDisabled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveTrustedSSID() {
|
||||||
|
if (currentText.isNotEmpty()) {
|
||||||
|
viewModel.onSaveTrustedSSID(currentText).let {
|
||||||
|
when (it) {
|
||||||
|
is Result.Success -> currentText = ""
|
||||||
|
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openSettings() {
|
||||||
|
scope.launch {
|
||||||
|
val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||||
|
intentSettings.data = Uri.fromParts("package", context.packageName, null)
|
||||||
|
context.startActivity(intentSettings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkFineLocationGranted() {
|
||||||
|
isBackgroundLocationGranted =
|
||||||
|
if (!fineLocationState.status.isGranted) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
viewModel.setLocationDisclosureShown()
|
||||||
|
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) {
|
AnimatedVisibility(showLocationServicesAlertDialog) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
|
@ -185,8 +228,9 @@ fun SettingsScreen(
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
showLocationServicesAlertDialog = false
|
showLocationServicesAlertDialog = false
|
||||||
viewModel.toggleAutoTunnel()
|
handleAutoTunnelToggle()
|
||||||
}) {
|
},
|
||||||
|
) {
|
||||||
Text(text = stringResource(R.string.okay))
|
Text(text = stringResource(R.string.okay))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -196,253 +240,313 @@ fun SettingsScreen(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
title = { Text(text = stringResource(R.string.location_services_not_detected)) },
|
title = { Text(text = stringResource(R.string.location_services_not_detected)) },
|
||||||
text = { Text(text = stringResource(R.string.location_services_missing_message)) })
|
text = { Text(text = stringResource(R.string.location_services_missing_message)) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!uiState.isLocationDisclosureShown) {
|
if (!uiState.isLocationDisclosureShown) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier.fillMaxSize().verticalScroll(scrollState).padding(padding)) {
|
modifier = Modifier.fillMaxSize().verticalScroll(scrollState).padding(padding),
|
||||||
Icon(
|
) {
|
||||||
Icons.Rounded.LocationOff,
|
Icon(
|
||||||
contentDescription = stringResource(id = R.string.map),
|
Icons.Rounded.LocationOff,
|
||||||
modifier = Modifier.padding(30.dp).size(128.dp))
|
contentDescription = stringResource(id = R.string.map),
|
||||||
Text(
|
modifier = Modifier.padding(30.dp).size(128.dp),
|
||||||
stringResource(R.string.prominent_background_location_title),
|
)
|
||||||
textAlign = TextAlign.Center,
|
Text(
|
||||||
modifier = Modifier.padding(30.dp),
|
stringResource(R.string.prominent_background_location_title),
|
||||||
fontSize = 20.sp)
|
textAlign = TextAlign.Center,
|
||||||
Text(
|
modifier = Modifier.padding(30.dp),
|
||||||
stringResource(R.string.prominent_background_location_message),
|
fontSize = 20.sp,
|
||||||
textAlign = TextAlign.Center,
|
)
|
||||||
modifier = Modifier.padding(30.dp),
|
Text(
|
||||||
fontSize = 15.sp)
|
stringResource(R.string.prominent_background_location_message),
|
||||||
Row(
|
textAlign = TextAlign.Center,
|
||||||
modifier =
|
modifier = Modifier.padding(30.dp),
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
fontSize = 15.sp,
|
||||||
Modifier.fillMaxWidth().padding(10.dp)
|
)
|
||||||
} else {
|
Row(
|
||||||
Modifier.fillMaxWidth().padding(30.dp)
|
modifier =
|
||||||
},
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
Modifier.fillMaxWidth().padding(10.dp)
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly) {
|
} else {
|
||||||
|
Modifier.fillMaxWidth().padding(30.dp)
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
) {
|
||||||
TextButton(onClick = { viewModel.setLocationDisclosureShown() }) {
|
TextButton(onClick = { viewModel.setLocationDisclosureShown() }) {
|
||||||
Text(stringResource(id = R.string.no_thanks))
|
Text(stringResource(id = R.string.no_thanks))
|
||||||
}
|
}
|
||||||
TextButton(
|
TextButton(
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
onClick = {
|
onClick = {
|
||||||
openSettings()
|
openSettings()
|
||||||
viewModel.setLocationDisclosureShown()
|
viewModel.setLocationDisclosureShown()
|
||||||
}) {
|
},
|
||||||
Text(stringResource(id = R.string.turn_on))
|
) {
|
||||||
}
|
Text(stringResource(id = R.string.turn_on))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(showAuthPrompt) {
|
if (showAuthPrompt) {
|
||||||
AuthorizationPrompt(
|
AuthorizationPrompt(
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
showAuthPrompt = false
|
showAuthPrompt = false
|
||||||
exportAllConfigs()
|
exportAllConfigs()
|
||||||
},
|
},
|
||||||
onError = { _ ->
|
onError = { _ ->
|
||||||
showAuthPrompt = false
|
showAuthPrompt = false
|
||||||
showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||||
},
|
},
|
||||||
onFailure = {
|
onFailure = {
|
||||||
showAuthPrompt = false
|
showAuthPrompt = false
|
||||||
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||||
})
|
},
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (uiState.tunnels.isEmpty() && uiState.isLocationDisclosureShown) {
|
if (uiState.tunnels.isEmpty() && uiState.isLocationDisclosureShown) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
modifier = Modifier.fillMaxSize().padding(padding)) {
|
modifier = Modifier.fillMaxSize().padding(padding),
|
||||||
Text(
|
) {
|
||||||
stringResource(R.string.one_tunnel_required),
|
Text(
|
||||||
textAlign = TextAlign.Center,
|
stringResource(R.string.one_tunnel_required),
|
||||||
modifier = Modifier.padding(15.dp),
|
textAlign = TextAlign.Center,
|
||||||
fontStyle = FontStyle.Italic)
|
modifier = Modifier.padding(15.dp),
|
||||||
|
fontStyle = FontStyle.Italic,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (uiState.isLocationDisclosureShown && uiState.tunnels.isNotEmpty()) {
|
if (uiState.isLocationDisclosureShown && uiState.tunnels.isNotEmpty()) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxSize().verticalScroll(scrollState).clickable(
|
Modifier.fillMaxSize().verticalScroll(scrollState).clickable(
|
||||||
indication = null, interactionSource = interactionSource) {
|
indication = null,
|
||||||
focusManager.clearFocus()
|
interactionSource = interactionSource,
|
||||||
}) {
|
) {
|
||||||
Surface(
|
focusManager.clearFocus()
|
||||||
tonalElevation = 2.dp,
|
},
|
||||||
shadowElevation = 2.dp,
|
) {
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
color = MaterialTheme.colorScheme.surface,
|
|
||||||
modifier =
|
|
||||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
|
||||||
Modifier.height(IntrinsicSize.Min)
|
|
||||||
.fillMaxWidth(fillMaxWidth)
|
|
||||||
.padding(top = 10.dp)
|
|
||||||
} else {
|
|
||||||
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 60.dp)
|
|
||||||
})
|
|
||||||
.padding(bottom = 10.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 =
|
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
|
||||||
checked = uiState.settings.isTunnelOnWifiEnabled,
|
|
||||||
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(
|
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.fillMaxWidth(fillMaxWidth).padding(vertical = 10.dp)) {
|
modifier =
|
||||||
Column(
|
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
horizontalAlignment = Alignment.Start,
|
Modifier.height(IntrinsicSize.Min)
|
||||||
verticalArrangement = Arrangement.Top,
|
.fillMaxWidth(fillMaxWidth)
|
||||||
modifier = Modifier.padding(15.dp)) {
|
.padding(top = 10.dp)
|
||||||
|
} else {
|
||||||
|
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 60.dp)
|
||||||
|
})
|
||||||
|
.padding(bottom = 10.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 =
|
||||||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
|
checked = uiState.settings.isTunnelOnWifiEnabled,
|
||||||
|
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 -> {
|
||||||
|
handleAutoTunnelToggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleAutoTunnelToggle()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
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(
|
SectionTitle(
|
||||||
title = stringResource(id = R.string.kernel), padding = screenPadding)
|
title = stringResource(id = R.string.kernel),
|
||||||
|
padding = screenPadding,
|
||||||
|
)
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(R.string.use_kernel),
|
stringResource(R.string.use_kernel),
|
||||||
enabled =
|
enabled =
|
||||||
|
@ -451,58 +555,70 @@ fun SettingsScreen(
|
||||||
(uiState.vpnState.status == Tunnel.State.UP)),
|
(uiState.vpnState.status == Tunnel.State.UP)),
|
||||||
checked = uiState.settings.isKernelEnabled,
|
checked = uiState.settings.isKernelEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = { viewModel.onToggleKernelMode().let {
|
onCheckChanged = {
|
||||||
when(it) {
|
viewModel.onToggleKernelMode().let {
|
||||||
is Result.Error -> showSnackbarMessage(it.error.message)
|
when (it) {
|
||||||
is Result.Success -> {}
|
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||||
|
is Result.Success -> {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} })
|
},
|
||||||
}
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
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 =
|
||||||
Modifier.fillMaxWidth(fillMaxWidth)
|
Modifier.fillMaxWidth(fillMaxWidth)
|
||||||
.padding(vertical = 10.dp)
|
.padding(vertical = 10.dp)
|
||||||
.padding(bottom = 140.dp)) {
|
.padding(bottom = 140.dp),
|
||||||
Column(
|
) {
|
||||||
horizontalAlignment = Alignment.Start,
|
Column(
|
||||||
verticalArrangement = Arrangement.Top,
|
horizontalAlignment = Alignment.Start,
|
||||||
modifier = Modifier.padding(15.dp)) {
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier = Modifier.padding(15.dp),
|
||||||
|
) {
|
||||||
SectionTitle(
|
SectionTitle(
|
||||||
title = stringResource(id = R.string.other), padding = screenPadding)
|
title = stringResource(id = R.string.other),
|
||||||
|
padding = screenPadding,
|
||||||
|
)
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(R.string.always_on_vpn_support),
|
stringResource(R.string.always_on_vpn_support),
|
||||||
enabled = !uiState.settings.isAutoTunnelEnabled,
|
enabled = !uiState.settings.isAutoTunnelEnabled,
|
||||||
checked = uiState.settings.isAlwaysOnVpnEnabled,
|
checked = uiState.settings.isAlwaysOnVpnEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = { viewModel.onToggleAlwaysOnVPN() })
|
onCheckChanged = { viewModel.onToggleAlwaysOnVPN() },
|
||||||
|
)
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(R.string.enabled_app_shortcuts),
|
stringResource(R.string.enabled_app_shortcuts),
|
||||||
enabled = true,
|
enabled = true,
|
||||||
checked = uiState.settings.isShortcutsEnabled,
|
checked = uiState.settings.isShortcutsEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = { viewModel.onToggleShortcutsEnabled() })
|
onCheckChanged = { viewModel.onToggleShortcutsEnabled() },
|
||||||
|
)
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
||||||
horizontalArrangement = Arrangement.Center) {
|
horizontalArrangement = Arrangement.Center,
|
||||||
TextButton(
|
) {
|
||||||
enabled = !didExportFiles, onClick = { showAuthPrompt = true }) {
|
TextButton(
|
||||||
Text(stringResource(R.string.export_configs))
|
enabled = !didExportFiles,
|
||||||
}
|
onClick = { showAuthPrompt = true },
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.export_configs))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Spacer(modifier = Modifier.weight(.17f))
|
Spacer(modifier = Modifier.weight(.17f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,10 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
||||||
|
|
||||||
data class SettingsUiState(
|
data class SettingsUiState(
|
||||||
val settings : Settings = Settings(),
|
val settings: Settings = Settings(),
|
||||||
val tunnels : List<TunnelConfig> = emptyList(),
|
val tunnels: List<TunnelConfig> = emptyList(),
|
||||||
val vpnState: VpnState = VpnState(),
|
val vpnState: VpnState = VpnState(),
|
||||||
val isLocationDisclosureShown : Boolean = true,
|
val isLocationDisclosureShown: Boolean = true,
|
||||||
val loading : Boolean = true
|
val isBatteryOptimizeDisableShown: Boolean = false,
|
||||||
|
val loading: Boolean = true
|
||||||
)
|
)
|
||||||
|
|
|
@ -36,18 +36,29 @@ constructor(
|
||||||
private val vpnService: VpnService
|
private val vpnService: VpnService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val uiState = combine(
|
val uiState =
|
||||||
settingsRepository.getSettingsFlow(),
|
combine(
|
||||||
tunnelConfigRepository.getTunnelConfigsFlow(),
|
settingsRepository.getSettingsFlow(),
|
||||||
vpnService.vpnState,
|
tunnelConfigRepository.getTunnelConfigsFlow(),
|
||||||
dataStoreManager.locationDisclosureFlow,
|
vpnService.vpnState,
|
||||||
){ settings, tunnels, tunnelState, locationDisclosure ->
|
dataStoreManager.preferencesFlow,
|
||||||
SettingsUiState(settings, tunnels, tunnelState, locationDisclosure
|
) { settings, tunnels, tunnelState, preferences ->
|
||||||
?: false, false)
|
SettingsUiState(
|
||||||
}.stateIn(viewModelScope,
|
settings,
|
||||||
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), SettingsUiState())
|
tunnels,
|
||||||
|
tunnelState,
|
||||||
|
preferences?.get(DataStoreManager.LOCATION_DISCLOSURE_SHOWN) ?: false,
|
||||||
|
preferences?.get(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN) ?: false,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.stateIn(
|
||||||
|
viewModelScope,
|
||||||
|
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
||||||
|
SettingsUiState(),
|
||||||
|
)
|
||||||
|
|
||||||
fun onSaveTrustedSSID(ssid: String) : Result<Unit>{
|
fun onSaveTrustedSSID(ssid: String): Result<Unit> {
|
||||||
val trimmed = ssid.trim()
|
val trimmed = ssid.trim()
|
||||||
return if (!uiState.value.settings.trustedNetworkSSIDs.contains(trimmed)) {
|
return if (!uiState.value.settings.trustedNetworkSSIDs.contains(trimmed)) {
|
||||||
uiState.value.settings.trustedNetworkSSIDs.add(trimmed)
|
uiState.value.settings.trustedNetworkSSIDs.add(trimmed)
|
||||||
|
@ -58,64 +69,77 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setLocationDisclosureShown() = viewModelScope.launch {
|
fun setLocationDisclosureShown() =
|
||||||
|
viewModelScope.launch {
|
||||||
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, true)
|
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setBatteryOptimizeDisableShown() =
|
||||||
|
viewModelScope.launch {
|
||||||
|
dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, true)
|
||||||
|
}
|
||||||
|
|
||||||
fun onToggleTunnelOnMobileData() {
|
fun onToggleTunnelOnMobileData() {
|
||||||
saveSettings(
|
saveSettings(
|
||||||
uiState.value.settings.copy(
|
uiState.value.settings.copy(
|
||||||
isTunnelOnMobileDataEnabled = !uiState.value.settings.isTunnelOnMobileDataEnabled
|
isTunnelOnMobileDataEnabled = !uiState.value.settings.isTunnelOnMobileDataEnabled,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDeleteTrustedSSID(ssid: String) {
|
fun onDeleteTrustedSSID(ssid: String) {
|
||||||
saveSettings(uiState.value.settings.copy(
|
|
||||||
trustedNetworkSSIDs = (uiState.value.settings.trustedNetworkSSIDs - ssid).toMutableList()
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getDefaultTunnelOrFirst() : String {
|
|
||||||
return uiState.value.settings.defaultTunnel ?: tunnelConfigRepository.getAll().first().toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toggleAutoTunnel() = viewModelScope.launch {
|
|
||||||
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
|
|
||||||
var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused
|
|
||||||
|
|
||||||
if (isAutoTunnelEnabled) {
|
|
||||||
ServiceManager.stopWatcherService(application)
|
|
||||||
} else {
|
|
||||||
ServiceManager.startWatcherService(application)
|
|
||||||
isAutoTunnelPaused = false
|
|
||||||
}
|
|
||||||
saveSettings(
|
saveSettings(
|
||||||
uiState.value.settings.copy(
|
uiState.value.settings.copy(
|
||||||
isAutoTunnelEnabled = !isAutoTunnelEnabled,
|
trustedNetworkSSIDs =
|
||||||
isAutoTunnelPaused = isAutoTunnelPaused,
|
(uiState.value.settings.trustedNetworkSSIDs - ssid).toMutableList(),
|
||||||
defaultTunnel = getDefaultTunnelOrFirst()
|
),
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun getDefaultTunnelOrFirst(): String {
|
||||||
|
return uiState.value.settings.defaultTunnel
|
||||||
|
?: tunnelConfigRepository.getAll().first().toString()
|
||||||
|
}
|
||||||
|
|
||||||
fun onToggleAlwaysOnVPN() = viewModelScope.launch {
|
fun toggleAutoTunnel() =
|
||||||
val updatedSettings = uiState.value.settings.copy(
|
viewModelScope.launch {
|
||||||
isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled,
|
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
|
||||||
defaultTunnel = getDefaultTunnelOrFirst()
|
var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused
|
||||||
|
|
||||||
|
if (isAutoTunnelEnabled) {
|
||||||
|
ServiceManager.stopWatcherService(application)
|
||||||
|
} else {
|
||||||
|
ServiceManager.startWatcherService(application)
|
||||||
|
isAutoTunnelPaused = false
|
||||||
|
}
|
||||||
|
saveSettings(
|
||||||
|
uiState.value.settings.copy(
|
||||||
|
isAutoTunnelEnabled = !isAutoTunnelEnabled,
|
||||||
|
isAutoTunnelPaused = isAutoTunnelPaused,
|
||||||
|
defaultTunnel = getDefaultTunnelOrFirst(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
saveSettings(updatedSettings)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveSettings(settings: Settings) = viewModelScope.launch {
|
fun onToggleAlwaysOnVPN() =
|
||||||
settingsRepository.save(settings)
|
viewModelScope.launch {
|
||||||
}
|
val updatedSettings =
|
||||||
|
uiState.value.settings.copy(
|
||||||
|
isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled,
|
||||||
|
defaultTunnel = getDefaultTunnelOrFirst(),
|
||||||
|
)
|
||||||
|
saveSettings(updatedSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveSettings(settings: Settings) =
|
||||||
|
viewModelScope.launch { settingsRepository.save(settings) }
|
||||||
|
|
||||||
fun onToggleTunnelOnEthernet() {
|
fun onToggleTunnelOnEthernet() {
|
||||||
saveSettings(uiState.value.settings.copy(
|
saveSettings(
|
||||||
isTunnelOnEthernetEnabled = !uiState.value.settings.isTunnelOnEthernetEnabled
|
uiState.value.settings.copy(
|
||||||
))
|
isTunnelOnEthernetEnabled = !uiState.value.settings.isTunnelOnEthernetEnabled,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isLocationEnabled(context: Context): Boolean {
|
fun isLocationEnabled(context: Context): Boolean {
|
||||||
|
@ -126,36 +150,36 @@ constructor(
|
||||||
fun onToggleShortcutsEnabled() {
|
fun onToggleShortcutsEnabled() {
|
||||||
saveSettings(
|
saveSettings(
|
||||||
uiState.value.settings.copy(
|
uiState.value.settings.copy(
|
||||||
isShortcutsEnabled = !uiState.value.settings.isShortcutsEnabled
|
isShortcutsEnabled = !uiState.value.settings.isShortcutsEnabled,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleBatterySaver() {
|
fun onToggleBatterySaver() {
|
||||||
saveSettings(
|
saveSettings(
|
||||||
uiState.value.settings.copy(
|
uiState.value.settings.copy(
|
||||||
isBatterySaverEnabled = !uiState.value.settings.isBatterySaverEnabled
|
isBatterySaverEnabled = !uiState.value.settings.isBatterySaverEnabled,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveKernelMode(on: Boolean) {
|
private fun saveKernelMode(on: Boolean) {
|
||||||
saveSettings(
|
saveSettings(
|
||||||
uiState.value.settings.copy(
|
uiState.value.settings.copy(
|
||||||
isKernelEnabled = on
|
isKernelEnabled = on,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleTunnelOnWifi() {
|
fun onToggleTunnelOnWifi() {
|
||||||
saveSettings(
|
saveSettings(
|
||||||
uiState.value.settings.copy(
|
uiState.value.settings.copy(
|
||||||
isTunnelOnWifiEnabled = !uiState.value.settings.isTunnelOnWifiEnabled
|
isTunnelOnWifiEnabled = !uiState.value.settings.isTunnelOnWifiEnabled,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleKernelMode() : Result<Unit> {
|
fun onToggleKernelMode(): Result<Unit> {
|
||||||
if (!uiState.value.settings.isKernelEnabled) {
|
if (!uiState.value.settings.isKernelEnabled) {
|
||||||
try {
|
try {
|
||||||
rootShell.start()
|
rootShell.start()
|
||||||
|
|
|
@ -62,39 +62,43 @@ fun SupportScreen(
|
||||||
showSnackbarMessage: (String) -> Unit,
|
showSnackbarMessage: (String) -> Unit,
|
||||||
focusRequester: FocusRequester
|
focusRequester: FocusRequester
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val fillMaxWidth = .85f
|
val fillMaxWidth = .85f
|
||||||
|
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
fun openWebPage(url: String) {
|
fun openWebPage(url: String) {
|
||||||
try {
|
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) {
|
} catch (e: Exception) {
|
||||||
showSnackbarMessage(Event.Error.Exception(e).message)
|
showSnackbarMessage(Event.Error.Exception(e).message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launchEmail() {
|
fun launchEmail() {
|
||||||
try {
|
try {
|
||||||
val intent =
|
val intent =
|
||||||
Intent(Intent.ACTION_SEND).apply {
|
Intent(Intent.ACTION_SENDTO).apply {
|
||||||
type = Constants.EMAIL_MIME_TYPE
|
type = Constants.EMAIL_MIME_TYPE
|
||||||
putExtra(Intent.EXTRA_EMAIL, context.getString(R.string.my_email))
|
putExtra(Intent.EXTRA_EMAIL, arrayOf(context.getString(R.string.my_email)))
|
||||||
putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
|
putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
|
||||||
}
|
}
|
||||||
startActivity(context, createChooser(intent, context.getString(R.string.email_chooser)), null)
|
startActivity(
|
||||||
} catch (e : Exception) {
|
context,
|
||||||
showSnackbarMessage(Event.Error.Exception(e).message)
|
createChooser(intent, context.getString(R.string.email_chooser)),
|
||||||
}
|
null,
|
||||||
}
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showSnackbarMessage(Event.Error.Exception(e).message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (uiState.loading) {
|
if (uiState.loading) {
|
||||||
LoadingScreen()
|
LoadingScreen()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
@ -103,126 +107,147 @@ fun SupportScreen(
|
||||||
Modifier.fillMaxSize()
|
Modifier.fillMaxSize()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.focusable()
|
.focusable()
|
||||||
.padding(padding)) {
|
.padding(padding),
|
||||||
Surface(
|
) {
|
||||||
tonalElevation = 2.dp,
|
Surface(
|
||||||
shadowElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
shape = RoundedCornerShape(12.dp),
|
shadowElevation = 2.dp,
|
||||||
color = MaterialTheme.colorScheme.surface,
|
shape = RoundedCornerShape(12.dp),
|
||||||
modifier =
|
color = MaterialTheme.colorScheme.surface,
|
||||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
modifier =
|
||||||
|
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Modifier.height(IntrinsicSize.Min)
|
Modifier.height(IntrinsicSize.Min)
|
||||||
.fillMaxWidth(fillMaxWidth)
|
.fillMaxWidth(fillMaxWidth)
|
||||||
.padding(top = 10.dp)
|
.padding(top = 10.dp)
|
||||||
} else {
|
} else {
|
||||||
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 20.dp)
|
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 20.dp)
|
||||||
})
|
})
|
||||||
.padding(bottom = 25.dp)) {
|
.padding(bottom = 25.dp),
|
||||||
Column(modifier = Modifier.padding(20.dp)) {
|
) {
|
||||||
Text(
|
Column(modifier = Modifier.padding(20.dp)) {
|
||||||
stringResource(R.string.thank_you),
|
Text(
|
||||||
textAlign = TextAlign.Start,
|
stringResource(R.string.thank_you),
|
||||||
fontWeight = FontWeight.Bold,
|
textAlign = TextAlign.Start,
|
||||||
modifier = Modifier.padding(bottom = 20.dp),
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 16.sp)
|
modifier = Modifier.padding(bottom = 20.dp),
|
||||||
Text(
|
fontSize = 16.sp,
|
||||||
stringResource(id = R.string.support_help_text),
|
)
|
||||||
textAlign = TextAlign.Start,
|
Text(
|
||||||
fontSize = 16.sp,
|
stringResource(id = R.string.support_help_text),
|
||||||
modifier = Modifier.padding(bottom = 20.dp))
|
textAlign = TextAlign.Start,
|
||||||
TextButton(
|
fontSize = 16.sp,
|
||||||
onClick = { openWebPage(context.resources.getString(R.string.docs_url)) },
|
modifier = Modifier.padding(bottom = 20.dp),
|
||||||
modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester)) {
|
)
|
||||||
Row(
|
TextButton(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
onClick = { openWebPage(context.resources.getString(R.string.docs_url)) },
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester),
|
||||||
modifier = Modifier.fillMaxWidth()) {
|
) {
|
||||||
Row {
|
Row(
|
||||||
Icon(Icons.Rounded.Book, stringResource(id = R.string.docs))
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
Text(
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
stringResource(id = R.string.docs_description),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
textAlign = TextAlign.Justify,
|
) {
|
||||||
modifier = Modifier.padding(start = 10.dp))
|
Row {
|
||||||
}
|
Icon(Icons.Rounded.Book, stringResource(id = R.string.docs))
|
||||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
Text(
|
||||||
}
|
stringResource(id = R.string.docs_description),
|
||||||
}
|
textAlign = TextAlign.Justify,
|
||||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
TextButton(
|
)
|
||||||
onClick = { openWebPage(context.resources.getString(R.string.discord_url)) },
|
}
|
||||||
modifier = Modifier.padding(vertical = 5.dp)) {
|
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||||
Row(
|
}
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxWidth()) {
|
|
||||||
Row {
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
|
||||||
TextButton(
|
|
||||||
onClick = { openWebPage(context.resources.getString(R.string.github_url)) },
|
|
||||||
modifier = Modifier.padding(vertical = 5.dp)) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxWidth()) {
|
|
||||||
Row {
|
|
||||||
Icon(
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
|
||||||
TextButton(
|
|
||||||
onClick = { launchEmail() }, modifier = Modifier.padding(vertical = 5.dp)) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxWidth()) {
|
|
||||||
Row {
|
|
||||||
Icon(Icons.Rounded.Mail, stringResource(id = R.string.email))
|
|
||||||
Text(
|
|
||||||
stringResource(id = R.string.email_description),
|
|
||||||
textAlign = TextAlign.Justify,
|
|
||||||
modifier = Modifier.padding(start = 10.dp))
|
|
||||||
}
|
|
||||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
TextButton(
|
||||||
Text(
|
onClick = { openWebPage(context.resources.getString(R.string.discord_url)) },
|
||||||
stringResource(id = R.string.privacy_policy),
|
modifier = Modifier.padding(vertical = 5.dp),
|
||||||
style = TextStyle(textDecoration = TextDecoration.Underline),
|
) {
|
||||||
fontSize = 16.sp,
|
Row(
|
||||||
modifier =
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
Modifier.clickable {
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
modifier = Modifier.fillMaxWidth(),
|
||||||
})
|
) {
|
||||||
Row(
|
Row {
|
||||||
horizontalArrangement = Arrangement.spacedBy(25.dp),
|
Icon(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
imageVector = ImageVector.vectorResource(R.drawable.discord),
|
||||||
modifier = Modifier.padding(25.dp)) {
|
stringResource(id = R.string.discord),
|
||||||
Text("Version: ${BuildConfig.VERSION_NAME}", modifier = Modifier.focusable())
|
Modifier.size(25.dp),
|
||||||
Text("Mode: ${if (uiState.settings.isKernelEnabled) "Kernel" else "Userspace" }")
|
)
|
||||||
}
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||||
|
TextButton(
|
||||||
|
onClick = { openWebPage(context.resources.getString(R.string.github_url)) },
|
||||||
|
modifier = Modifier.padding(vertical = 5.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Icon(
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||||
|
TextButton(
|
||||||
|
onClick = { launchEmail() },
|
||||||
|
modifier = Modifier.padding(vertical = 5.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Icon(Icons.Rounded.Mail, stringResource(id = R.string.email))
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.email_description),
|
||||||
|
textAlign = TextAlign.Justify,
|
||||||
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.privacy_policy),
|
||||||
|
style = TextStyle(textDecoration = TextDecoration.Underline),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
modifier =
|
||||||
|
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"}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,4 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.support
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
|
|
||||||
data class SupportUiState(
|
data class SupportUiState(val settings: Settings = Settings(), val loading: Boolean = true)
|
||||||
val settings : Settings = Settings(),
|
|
||||||
val loading : Boolean = true
|
|
||||||
)
|
|
||||||
|
|
|
@ -11,15 +11,16 @@ import kotlinx.coroutines.flow.stateIn
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SupportViewModel @Inject constructor(
|
class SupportViewModel @Inject constructor(private val settingsRepository: SettingsRepository) :
|
||||||
private val settingsRepository: SettingsRepository
|
ViewModel() {
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
val uiState = settingsRepository.getSettingsFlow().map {
|
val uiState =
|
||||||
SupportUiState(it, false)
|
settingsRepository
|
||||||
}.stateIn(
|
.getSettingsFlow()
|
||||||
viewModelScope,
|
.map { SupportUiState(it, false) }
|
||||||
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
.stateIn(
|
||||||
SupportUiState()
|
viewModelScope,
|
||||||
)
|
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
||||||
|
SupportUiState(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ private val DarkColorScheme =
|
||||||
primary = virdigris,
|
primary = virdigris,
|
||||||
secondary = virdigris,
|
secondary = virdigris,
|
||||||
// secondary = PurpleGrey80,
|
// secondary = PurpleGrey80,
|
||||||
tertiary = virdigris
|
tertiary = virdigris,
|
||||||
// tertiary = Pink80
|
// tertiary = Pink80
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ 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),
|
||||||
|
@ -57,7 +57,6 @@ fun WireguardAutoTunnelTheme(
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
darkTheme -> DarkColorScheme
|
darkTheme -> DarkColorScheme
|
||||||
else -> LightColorScheme
|
else -> LightColorScheme
|
||||||
}
|
}
|
||||||
|
@ -68,14 +67,19 @@ fun WireguardAutoTunnelTheme(
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
window.statusBarColor = Color.Transparent.toArgb()
|
window.statusBarColor = Color.Transparent.toArgb()
|
||||||
window.navigationBarColor = Color.Transparent.toArgb()
|
window.navigationBarColor = Color.Transparent.toArgb()
|
||||||
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = !darkTheme
|
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars =
|
||||||
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightNavigationBars = !darkTheme
|
!darkTheme
|
||||||
|
WindowCompat.getInsetsController(
|
||||||
|
window,
|
||||||
|
window.decorView,
|
||||||
|
)
|
||||||
|
.isAppearanceLightNavigationBars = !darkTheme
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
typography = Typography,
|
typography = Typography,
|
||||||
content = content
|
content = content,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ fun TransparentSystemBars() {
|
||||||
DisposableEffect(systemUiController, useDarkIcons) {
|
DisposableEffect(systemUiController, useDarkIcons) {
|
||||||
systemUiController.setSystemBarsColor(
|
systemUiController.setSystemBarsColor(
|
||||||
color = Color.Transparent,
|
color = Color.Transparent,
|
||||||
darkIcons = useDarkIcons
|
darkIcons = useDarkIcons,
|
||||||
)
|
)
|
||||||
|
|
||||||
onDispose {}
|
onDispose {}
|
||||||
|
|
|
@ -10,13 +10,13 @@ import androidx.compose.ui.unit.sp
|
||||||
val Typography =
|
val Typography =
|
||||||
Typography(
|
Typography(
|
||||||
bodyLarge =
|
bodyLarge =
|
||||||
TextStyle(
|
TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
lineHeight = 24.sp,
|
lineHeight = 24.sp,
|
||||||
letterSpacing = 0.5.sp
|
letterSpacing = 0.5.sp,
|
||||||
)
|
),
|
||||||
/* Other default text styles to override
|
/* Other default text styles to override
|
||||||
titleLarge = TextStyle(
|
titleLarge = TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
|
|
|
@ -12,79 +12,100 @@ sealed class Event {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_none)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_none)
|
||||||
}
|
}
|
||||||
|
|
||||||
data object SsidConflict : Error() {
|
data object SsidConflict : Error() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_ssid_exists)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_ssid_exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
data object RootDenied : Error() {
|
data object RootDenied : Error() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_root_denied)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_root_denied)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class General(val customMessage: String) : Error() {
|
data class General(val customMessage: String) : Error() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = customMessage
|
get() = customMessage
|
||||||
}
|
}
|
||||||
data class Exception(val exception : kotlin.Exception) : Error() {
|
|
||||||
|
data class Exception(val exception: kotlin.Exception) : Error() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = exception.message ?: WireGuardAutoTunnel.instance.getString(R.string.unknown_error)
|
get() =
|
||||||
|
exception.message
|
||||||
|
?: WireGuardAutoTunnel.instance.getString(R.string.unknown_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
data object InvalidQrCode : Error() {
|
data object InvalidQrCode : Error() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_invalid_code)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_invalid_code)
|
||||||
}
|
}
|
||||||
|
|
||||||
data object InvalidFileExtension : Error() {
|
data object InvalidFileExtension : Error() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
data object FileReadFailed : Error() {
|
data object FileReadFailed : Error() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
data object AuthenticationFailed : Error() {
|
data object AuthenticationFailed : Error() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authentication_failed)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authentication_failed)
|
||||||
}
|
}
|
||||||
|
|
||||||
data object AuthorizationFailed : Error() {
|
data object AuthorizationFailed : Error() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authorization_failed)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authorization_failed)
|
||||||
}
|
}
|
||||||
|
|
||||||
data object BackgroundLocationRequired : Error() {
|
data object BackgroundLocationRequired : Error() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.background_location_required)
|
get() =
|
||||||
|
WireGuardAutoTunnel.instance.getString(R.string.background_location_required)
|
||||||
}
|
}
|
||||||
|
|
||||||
data object LocationServicesRequired : Error() {
|
data object LocationServicesRequired : Error() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.location_services_required)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.location_services_required)
|
||||||
}
|
}
|
||||||
|
|
||||||
data object PreciseLocationRequired : Error() {
|
data object PreciseLocationRequired : Error() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.precise_location_required)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.precise_location_required)
|
||||||
}
|
}
|
||||||
|
|
||||||
data object FileExplorerRequired : Error() {
|
data object FileExplorerRequired : Error() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_no_file_explorer)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_no_file_explorer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class Message : Event() {
|
sealed class Message : Event() {
|
||||||
data object ConfigSaved: Message() {
|
data object ConfigSaved : Message() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.config_changes_saved)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.config_changes_saved)
|
||||||
}
|
}
|
||||||
data object ConfigsExported: Message() {
|
|
||||||
|
data object ConfigsExported : Message() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.exported_configs_message)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.exported_configs_message)
|
||||||
}
|
}
|
||||||
data object TunnelOffAction: Message() {
|
|
||||||
|
data object TunnelOffAction : Message() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_tunnel)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_tunnel)
|
||||||
}
|
}
|
||||||
data object TunnelOnAction: Message() {
|
|
||||||
|
data object TunnelOnAction : Message() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_on_tunnel)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_on_tunnel)
|
||||||
}
|
}
|
||||||
data object AutoTunnelOffAction: Message() {
|
|
||||||
|
data object AutoTunnelOffAction : Message() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_auto)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_auto)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,23 +37,23 @@ fun BigDecimal.toThreeDecimalPlaceString(): String {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> List<T>.update(index: Int, item: T): List<T> = toMutableList().apply { this[index] = item }
|
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) }
|
fun <T> List<T>.removeAt(index: Int): List<T> = toMutableList().apply { this.removeAt(index) }
|
||||||
|
|
||||||
typealias TunnelConfigs = List<TunnelConfig>
|
typealias TunnelConfigs = List<TunnelConfig>
|
||||||
|
|
||||||
typealias Packages = List<PackageInfo>
|
typealias Packages = List<PackageInfo>
|
||||||
|
|
||||||
fun Statistics.mapPeerStats(): Map<Key, PeerStats?> {
|
fun Statistics.mapPeerStats(): Map<Key, PeerStats?> {
|
||||||
return this.peers().associateWith { key ->
|
return this.peers().associateWith { key -> (this.peer(key)) }
|
||||||
(this.peer(key))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun PeerStats.latestHandshakeSeconds() : Long? {
|
fun PeerStats.latestHandshakeSeconds(): Long? {
|
||||||
return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis)
|
return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun PeerStats.handshakeStatus() : HandshakeStatus {
|
fun PeerStats.handshakeStatus(): HandshakeStatus {
|
||||||
//TODO add never connected status after duration
|
// TODO add never connected status after duration
|
||||||
return this.latestHandshakeSeconds().let {
|
return this.latestHandshakeSeconds().let {
|
||||||
when {
|
when {
|
||||||
it == null -> HandshakeStatus.NOT_STARTED
|
it == null -> HandshakeStatus.NOT_STARTED
|
||||||
|
@ -65,4 +65,3 @@ fun PeerStats.handshakeStatus() : HandshakeStatus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,22 +36,20 @@ object FileUtils {
|
||||||
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 =
|
||||||
files: List<File>
|
createDownloadsFileOutputStream(
|
||||||
) {
|
context,
|
||||||
val zipOutputStream = createDownloadsFileOutputStream(
|
"wg-export_${Instant.now().epochSecond}.zip",
|
||||||
context,
|
ZIP_FILE_MIME_TYPE,
|
||||||
"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)
|
||||||
|
|
|
@ -2,15 +2,15 @@ package com.zaneschepke.wireguardautotunnel.util
|
||||||
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
||||||
sealed class Result<T> {
|
sealed class Result<T> {
|
||||||
class Success<T>(val data: T): Result<T>()
|
class Success<T>(val data: T) : Result<T>()
|
||||||
class Error<T>(val error : Event.Error): Result<T>() {
|
|
||||||
|
class Error<T>(val error: Event.Error) : Result<T>() {
|
||||||
init {
|
init {
|
||||||
when(this.error) {
|
when (this.error) {
|
||||||
is Event.Error.Exception -> Timber.e(this.error.exception)
|
is Event.Error.Exception -> Timber.e(this.error.exception)
|
||||||
else -> Timber.e(this.error.message)
|
else -> Timber.e(this.error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
android:height="800dp"
|
android:height="800dp"
|
||||||
android:viewportWidth="256"
|
android:viewportWidth="256"
|
||||||
android:viewportHeight="256">
|
android:viewportHeight="256">
|
||||||
<path
|
<path
|
||||||
android:pathData="M216.86,45.1C200.29,37.34 182.57,31.71 164.04,28.5C161.77,32.61 159.11,38.15 157.28,42.55C137.58,39.58 118.07,39.58 98.74,42.55C96.91,38.15 94.19,32.61 91.9,28.5C73.35,31.71 55.61,37.36 39.04,45.14C5.62,95.65 -3.44,144.9 1.09,193.46C23.26,210.01 44.74,220.07 65.86,226.65C71.08,219.47 75.73,211.84 79.74,203.8C72.1,200.9 64.79,197.32 57.89,193.17C59.72,191.81 61.51,190.39 63.24,188.93C105.37,208.63 151.13,208.63 192.75,188.93C194.51,190.39 196.3,191.81 198.11,193.17C191.18,197.34 183.85,200.92 176.22,203.82C180.23,211.84 184.86,219.49 190.1,226.67C211.24,220.09 232.74,210.03 254.91,193.46C260.23,137.17 245.83,88.37 216.86,45.1ZM85.47,163.59C72.83,163.59 62.46,151.79 62.46,137.41C62.46,123.04 72.61,111.21 85.47,111.21C98.34,111.21 108.71,123.02 108.49,137.41C108.51,151.79 98.34,163.59 85.47,163.59ZM170.53,163.59C157.88,163.59 147.51,151.79 147.51,137.41C147.51,123.04 157.66,111.21 170.53,111.21C183.39,111.21 193.76,123.02 193.54,137.41C193.54,151.79 183.39,163.59 170.53,163.59Z"
|
android:fillColor="#5865F2"
|
||||||
android:fillColor="#5865F2"
|
android:fillType="nonZero"
|
||||||
android:fillType="nonZero"/>
|
android:pathData="M216.86,45.1C200.29,37.34 182.57,31.71 164.04,28.5C161.77,32.61 159.11,38.15 157.28,42.55C137.58,39.58 118.07,39.58 98.74,42.55C96.91,38.15 94.19,32.61 91.9,28.5C73.35,31.71 55.61,37.36 39.04,45.14C5.62,95.65 -3.44,144.9 1.09,193.46C23.26,210.01 44.74,220.07 65.86,226.65C71.08,219.47 75.73,211.84 79.74,203.8C72.1,200.9 64.79,197.32 57.89,193.17C59.72,191.81 61.51,190.39 63.24,188.93C105.37,208.63 151.13,208.63 192.75,188.93C194.51,190.39 196.3,191.81 198.11,193.17C191.18,197.34 183.85,200.92 176.22,203.82C180.23,211.84 184.86,219.49 190.1,226.67C211.24,220.09 232.74,210.03 254.91,193.46C260.23,137.17 245.83,88.37 216.86,45.1ZM85.47,163.59C72.83,163.59 62.46,151.79 62.46,137.41C62.46,123.04 72.61,111.21 85.47,111.21C98.34,111.21 108.71,123.02 108.49,137.41C108.51,151.79 98.34,163.59 85.47,163.59ZM170.53,163.59C157.88,163.59 147.51,151.79 147.51,137.41C147.51,123.04 157.66,111.21 170.53,111.21C183.39,111.21 193.76,123.02 193.54,137.41C193.54,151.79 183.39,163.59 170.53,163.59Z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
android:height="800dp"
|
android:height="800dp"
|
||||||
android:viewportWidth="20"
|
android:viewportWidth="20"
|
||||||
android:viewportHeight="20">
|
android:viewportHeight="20">
|
||||||
<path
|
<path
|
||||||
android:pathData="M10,0C15.523,0 20,4.59 20,10.253C20,14.782 17.138,18.624 13.167,19.981C12.66,20.082 12.48,19.762 12.48,19.489C12.48,19.151 12.492,18.047 12.492,16.675C12.492,15.719 12.172,15.095 11.813,14.777C14.04,14.523 16.38,13.656 16.38,9.718C16.38,8.598 15.992,7.684 15.35,6.966C15.454,6.707 15.797,5.664 15.252,4.252C15.252,4.252 14.414,3.977 12.505,5.303C11.706,5.076 10.85,4.962 10,4.958C9.15,4.962 8.295,5.076 7.497,5.303C5.586,3.977 4.746,4.252 4.746,4.252C4.203,5.664 4.546,6.707 4.649,6.966C4.01,7.684 3.619,8.598 3.619,9.718C3.619,13.646 5.954,14.526 8.175,14.785C7.889,15.041 7.63,15.493 7.54,16.156C6.97,16.418 5.522,16.871 4.63,15.304C4.63,15.304 4.101,14.319 3.097,14.247C3.097,14.247 2.122,14.234 3.029,14.87C3.029,14.87 3.684,15.185 4.139,16.37C4.139,16.37 4.726,18.2 7.508,17.58C7.513,18.437 7.522,19.245 7.522,19.489C7.522,19.76 7.338,20.077 6.839,19.982C2.865,18.627 0,14.783 0,10.253C0,4.59 4.478,0 10,0"
|
android:fillColor="#000000"
|
||||||
android:strokeWidth="1"
|
android:fillType="evenOdd"
|
||||||
android:fillColor="#000000"
|
android:pathData="M10,0C15.523,0 20,4.59 20,10.253C20,14.782 17.138,18.624 13.167,19.981C12.66,20.082 12.48,19.762 12.48,19.489C12.48,19.151 12.492,18.047 12.492,16.675C12.492,15.719 12.172,15.095 11.813,14.777C14.04,14.523 16.38,13.656 16.38,9.718C16.38,8.598 15.992,7.684 15.35,6.966C15.454,6.707 15.797,5.664 15.252,4.252C15.252,4.252 14.414,3.977 12.505,5.303C11.706,5.076 10.85,4.962 10,4.958C9.15,4.962 8.295,5.076 7.497,5.303C5.586,3.977 4.746,4.252 4.746,4.252C4.203,5.664 4.546,6.707 4.649,6.966C4.01,7.684 3.619,8.598 3.619,9.718C3.619,13.646 5.954,14.526 8.175,14.785C7.889,15.041 7.63,15.493 7.54,16.156C6.97,16.418 5.522,16.871 4.63,15.304C4.63,15.304 4.101,14.319 3.097,14.247C3.097,14.247 2.122,14.234 3.029,14.87C3.029,14.87 3.684,15.185 4.139,16.37C4.139,16.37 4.726,18.2 7.508,17.58C7.513,18.437 7.522,19.245 7.522,19.489C7.522,19.76 7.338,20.077 6.839,19.982C2.865,18.627 0,14.783 0,10.253C0,4.59 4.478,0 10,0"
|
||||||
android:fillType="evenOdd"
|
android:strokeWidth="1"
|
||||||
android:strokeColor="#00000000"/>
|
android:strokeColor="#00000000" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
<vector android:height="24dp" android:tint="#000000"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:viewportHeight="24" android:viewportWidth="24"
|
android:width="24dp"
|
||||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
android:height="24dp"
|
||||||
<path android:fillColor="@android:color/white" android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12V5l-9,-4z"/>
|
android:tint="#000000"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12V5l-9,-4z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
<vector android:height="24dp" android:tint="#000000"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:viewportHeight="24" android:viewportWidth="24"
|
android:width="24dp"
|
||||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
android:height="24dp"
|
||||||
<path android:fillColor="@android:color/white" android:pathData="M20.83,18H21v-4h2v-4H12.83L20.83,18zM19.78,22.61l1.41,-1.41L2.81,2.81L1.39,4.22l2.59,2.59C2.2,7.85 1,9.79 1,12c0,3.31 2.69,6 6,6c2.21,0 4.15,-1.2 5.18,-2.99L19.78,22.61zM8.99,11.82C9,11.88 9,11.94 9,12c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2s0.9,-2 2,-2c0.06,0 0.12,0 0.18,0.01L8.99,11.82z"/>
|
android:tint="#000000"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M20.83,18H21v-4h2v-4H12.83L20.83,18zM19.78,22.61l1.41,-1.41L2.81,2.81L1.39,4.22l2.59,2.59C2.2,7.85 1,9.79 1,12c0,3.31 2.69,6 6,6c2.21,0 4.15,-1.2 5.18,-2.99L19.78,22.61zM8.99,11.82C9,11.88 9,11.94 9,12c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2s0.9,-2 2,-2c0.06,0 0.12,0 0.18,0.01L8.99,11.82z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
<vector android:height="24dp" android:tint="#000000"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:viewportHeight="24" android:viewportWidth="24"
|
android:width="24dp"
|
||||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
android:height="24dp"
|
||||||
<path android:fillColor="@android:color/white" android:pathData="M12.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z"/>
|
android:tint="#000000"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M12.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_banner_background"/>
|
<background android:drawable="@color/ic_banner_background" />
|
||||||
<foreground android:drawable="@mipmap/ic_banner_foreground"/>
|
<foreground android:drawable="@mipmap/ic_banner_foreground" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background" />
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background" />
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
|
@ -164,4 +164,7 @@
|
||||||
<string name="location_service_missing">Location Services Not Detected</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="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>
|
<string name="auto_tunnel_title">Auto-tunnel Service</string>
|
||||||
|
<string name="delete_tunnel">Delete tunnel</string>
|
||||||
|
<string name="delete_tunnel_message">Are you sure you would like to delete this tunnel?</string>
|
||||||
|
<string name="yes">Yes</string>
|
||||||
</resources>
|
</resources>
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<style name="Theme.WireguardAutoTunnel" parent="@style/Theme.AppCompat.NoActionBar">
|
<style name="Theme.WireguardAutoTunnel" parent="@style/Theme.AppCompat.NoActionBar">
|
||||||
<item name="android:windowBackground">@color/black_background</item>
|
<item name="android:windowBackground">@color/black_background</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,31 +1,35 @@
|
||||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<shortcut
|
<shortcut
|
||||||
android:shortcutId="defaultOn1"
|
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:icon="@drawable/vpn_on"
|
android:icon="@drawable/vpn_on"
|
||||||
android:shortcutShortLabel="@string/vpn_on"
|
android:shortcutDisabledMessage="@string/vpn_on"
|
||||||
|
android:shortcutId="defaultOn1"
|
||||||
android:shortcutLongLabel="@string/default_vpn_on"
|
android:shortcutLongLabel="@string/default_vpn_on"
|
||||||
android:shortcutDisabledMessage="@string/vpn_on">
|
android:shortcutShortLabel="@string/vpn_on">
|
||||||
<intent
|
<intent
|
||||||
android:action="START"
|
android:action="START"
|
||||||
android:targetPackage="com.zaneschepke.wireguardautotunnel"
|
android:targetClass="com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsActivity"
|
||||||
android:targetClass="com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsActivity">
|
android:targetPackage="com.zaneschepke.wireguardautotunnel">
|
||||||
<extra android:name="className" android:value="WireGuardTunnelService" />
|
<extra
|
||||||
|
android:name="className"
|
||||||
|
android:value="WireGuardTunnelService" />
|
||||||
</intent>
|
</intent>
|
||||||
<capability-binding android:key="actions.intent.START" />
|
<capability-binding android:key="actions.intent.START" />
|
||||||
</shortcut>
|
</shortcut>
|
||||||
<shortcut
|
<shortcut
|
||||||
android:shortcutId="defaultOff1"
|
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:icon="@drawable/vpn_off"
|
android:icon="@drawable/vpn_off"
|
||||||
android:shortcutShortLabel="@string/vpn_off"
|
android:shortcutDisabledMessage="@string/vpn_off"
|
||||||
|
android:shortcutId="defaultOff1"
|
||||||
android:shortcutLongLabel="@string/default_vpn_off"
|
android:shortcutLongLabel="@string/default_vpn_off"
|
||||||
android:shortcutDisabledMessage="@string/vpn_off">
|
android:shortcutShortLabel="@string/vpn_off">
|
||||||
<intent
|
<intent
|
||||||
android:action="STOP"
|
android:action="STOP"
|
||||||
android:targetPackage="com.zaneschepke.wireguardautotunnel"
|
android:targetClass="com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsActivity"
|
||||||
android:targetClass="com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsActivity">
|
android:targetPackage="com.zaneschepke.wireguardautotunnel">
|
||||||
<extra android:name="className" android:value="WireGuardTunnelService" />
|
<extra
|
||||||
|
android:name="className"
|
||||||
|
android:value="WireGuardTunnelService" />
|
||||||
</intent>
|
</intent>
|
||||||
<capability-binding android:key="actions.intent.STOP" />
|
<capability-binding android:key="actions.intent.STOP" />
|
||||||
</shortcut>
|
</shortcut>
|
||||||
|
|
|
@ -26,11 +26,9 @@ object BuildHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isReleaseBuild(gradle: Gradle): Boolean {
|
fun isReleaseBuild(gradle: Gradle): Boolean {
|
||||||
return (
|
return (gradle.startParameter.taskNames.size > 0 &&
|
||||||
gradle.startParameter.taskNames.size > 0 &&
|
gradle.startParameter.taskNames[0].contains(
|
||||||
gradle.startParameter.taskNames[0].contains(
|
"Release",
|
||||||
"Release",
|
))
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
object Constants {
|
object Constants {
|
||||||
const val VERSION_NAME = "3.3.2"
|
const val VERSION_NAME = "3.3.3"
|
||||||
const val JVM_TARGET = "17"
|
const val JVM_TARGET = "17"
|
||||||
const val VERSION_CODE = 33200
|
const val VERSION_CODE = 33300
|
||||||
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"
|
||||||
const val APP_NAME = "wgtunnel"
|
const val APP_NAME = "wgtunnel"
|
||||||
|
const val COMPOSE_COMPILER_EXTENSION_VERSION = "1.5.7"
|
||||||
|
|
||||||
|
|
||||||
const val STORE_PASS_VAR = "SIGNING_STORE_PASSWORD"
|
const val STORE_PASS_VAR = "SIGNING_STORE_PASSWORD"
|
||||||
const val KEY_ALIAS_VAR = "SIGNING_KEY_ALIAS"
|
const val KEY_ALIAS_VAR = "SIGNING_KEY_ALIAS"
|
||||||
|
|
|
@ -5,13 +5,13 @@ platform :android do
|
||||||
desc "Deploy a beta version to the Google Play"
|
desc "Deploy a beta version to the Google Play"
|
||||||
lane :beta do
|
lane :beta do
|
||||||
gradle(task: "clean bundleGeneralRelease")
|
gradle(task: "clean bundleGeneralRelease")
|
||||||
upload_to_play_store(track: 'beta')
|
upload_to_play_store(track: 'beta', skip_upload_apk: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "Deploy a new version to the Google Play"
|
desc "Deploy a new version to the Google Play"
|
||||||
lane :production do
|
lane :production do
|
||||||
gradle(task: "clean bundleGeneralRelease")
|
gradle(task: "clean bundleGeneralRelease")
|
||||||
upload_to_play_store
|
upload_to_play_store(skip_upload_apk: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
|
@ -0,0 +1,7 @@
|
||||||
|
Enhancements:
|
||||||
|
- Added delete tunnel confirmation
|
||||||
|
- Added battery background permission
|
||||||
|
Fixes:
|
||||||
|
- Tunnel disable frozen bug
|
||||||
|
- Email to field bug
|
||||||
|
- Config edit empty DNS
|
|
@ -11,7 +11,7 @@ desugar_jdk_libs = "2.0.4"
|
||||||
espressoCore = "3.5.1"
|
espressoCore = "3.5.1"
|
||||||
firebase-crashlytics-gradle = "2.9.9"
|
firebase-crashlytics-gradle = "2.9.9"
|
||||||
google-services = "4.4.0"
|
google-services = "4.4.0"
|
||||||
hiltAndroid = "2.49"
|
hiltAndroid = "2.50"
|
||||||
hiltNavigationCompose = "1.1.0"
|
hiltNavigationCompose = "1.1.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
kotlinx-serialization-json = "1.6.2"
|
kotlinx-serialization-json = "1.6.2"
|
||||||
|
@ -22,15 +22,14 @@ navigationCompose = "2.7.6"
|
||||||
roomVersion = "2.6.1"
|
roomVersion = "2.6.1"
|
||||||
timber = "5.0.1"
|
timber = "5.0.1"
|
||||||
tunnel = "1.0.20230706"
|
tunnel = "1.0.20230706"
|
||||||
androidGradlePlugin = "8.2.0"
|
androidGradlePlugin = "8.2.1"
|
||||||
kotlin="1.9.10"
|
kotlin = "1.9.21"
|
||||||
ksp="1.9.10-1.0.13"
|
ksp = "1.9.21-1.0.16"
|
||||||
composeBom="2023.10.01"
|
composeBom = "2023.10.01"
|
||||||
firebaseBom= "32.7.0"
|
firebaseBom = "32.7.0"
|
||||||
compose="1.5.4"
|
compose = "1.5.4"
|
||||||
crashlytics= "18.6.0"
|
crashlytics = "18.6.0"
|
||||||
analytics="21.5.0"
|
analytics = "21.5.0"
|
||||||
composeCompiler="1.5.3"
|
|
||||||
zxingAndroidEmbedded = "4.3.0"
|
zxingAndroidEmbedded = "4.3.0"
|
||||||
zxingCore = "3.5.2"
|
zxingCore = "3.5.2"
|
||||||
|
|
||||||
|
@ -54,13 +53,13 @@ androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomVers
|
||||||
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomVersion" }
|
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomVersion" }
|
||||||
|
|
||||||
#compose
|
#compose
|
||||||
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref="composeBom" }
|
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
|
||||||
androidx-compose-ui-test = { module="androidx.compose.ui:ui-test-junit4", version.ref="compose" }
|
androidx-compose-ui-test = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose" }
|
||||||
androidx-compose-ui-tooling = { module="androidx.compose.ui:ui-tooling", version.ref="compose" }
|
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
|
||||||
androidx-compose-manifest = { module="androidx.compose.ui:ui-test-manifest", version.ref="compose" }
|
androidx-compose-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "compose" }
|
||||||
androidx-compose-ui-graphics = { module="androidx.compose.ui:ui-graphics", version.ref="compose" }
|
androidx-compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics", version.ref = "compose" }
|
||||||
androidx-compose-ui-tooling-preview = { module="androidx.compose.ui:ui-tooling-preview", version.ref="compose" }
|
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" }
|
||||||
androidx-compose-ui = { module="androidx.compose.ui:ui", version.ref="compose" }
|
androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" }
|
||||||
|
|
||||||
#hilt
|
#hilt
|
||||||
androidx-room-testing = { module = "androidx.room:room-testing", version.ref = "roomVersion" }
|
androidx-room-testing = { module = "androidx.room:room-testing", version.ref = "roomVersion" }
|
||||||
|
@ -84,14 +83,13 @@ lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-com
|
||||||
material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "material-icons-extended" }
|
material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "material-icons-extended" }
|
||||||
|
|
||||||
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
|
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
|
||||||
|
|
||||||
tunnel = { module = "com.wireguard.android:tunnel", version.ref = "tunnel" }
|
tunnel = { module = "com.wireguard.android:tunnel", version.ref = "tunnel" }
|
||||||
|
|
||||||
#firebase
|
#firebase
|
||||||
google-firebase-crashlytics-ktx = { module = "com.google.firebase:firebase-crashlytics-ktx", version.ref = "crashlytics" }
|
google-firebase-crashlytics-ktx = { module = "com.google.firebase:firebase-crashlytics-ktx", version.ref = "crashlytics" }
|
||||||
google-firebase-analytics-ktx = { module = "com.google.firebase:firebase-analytics-ktx", version.ref = "analytics" }
|
google-firebase-analytics-ktx = { module = "com.google.firebase:firebase-analytics-ktx", version.ref = "analytics" }
|
||||||
firebase-crashlytics-gradle = { module = "com.google.firebase:firebase-crashlytics-gradle", version.ref = "firebase-crashlytics-gradle" }
|
firebase-crashlytics-gradle = { module = "com.google.firebase:firebase-crashlytics-gradle", version.ref = "firebase-crashlytics-gradle" }
|
||||||
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom"}
|
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
|
||||||
google-services = { module = "com.google.gms:google-services", version.ref = "google-services" }
|
google-services = { module = "com.google.gms:google-services", version.ref = "google-services" }
|
||||||
|
|
||||||
zxing-core = { module = "com.google.zxing:core", version.ref = "zxingCore" }
|
zxing-core = { module = "com.google.zxing:core", version.ref = "zxingCore" }
|
||||||
|
@ -101,4 +99,4 @@ zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", ve
|
||||||
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
|
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
|
||||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hiltAndroid" }
|
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hiltAndroid" }
|
||||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
|
@ -5,6 +5,7 @@ pluginManagement {
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencyResolutionManagement {
|
dependencyResolutionManagement {
|
||||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
repositories {
|
repositories {
|
||||||
|
@ -14,4 +15,5 @@ dependencyResolutionManagement {
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.name = "WG Tunnel"
|
rootProject.name = "WG Tunnel"
|
||||||
|
|
||||||
include(":app")
|
include(":app")
|
||||||
|
|
Loading…
Reference in New Issue