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.
This commit is contained in:
parent
7ec294b789
commit
3339448424
|
@ -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 '....'
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,7 +67,6 @@ 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
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
@ -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,24 +43,33 @@ 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 =
|
||||||
|
file(
|
||||||
|
System.getenv()
|
||||||
|
.getOrDefault(
|
||||||
Constants.KEY_STORE_PATH_VAR,
|
Constants.KEY_STORE_PATH_VAR,
|
||||||
properties.getProperty(Constants.KEY_STORE_PATH_VAR)
|
properties.getProperty(Constants.KEY_STORE_PATH_VAR),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
storePassword =
|
||||||
storePassword = System.getenv().getOrDefault(
|
System.getenv()
|
||||||
|
.getOrDefault(
|
||||||
Constants.STORE_PASS_VAR,
|
Constants.STORE_PASS_VAR,
|
||||||
properties.getProperty(Constants.STORE_PASS_VAR)
|
properties.getProperty(Constants.STORE_PASS_VAR),
|
||||||
)
|
)
|
||||||
keyAlias = System.getenv().getOrDefault(
|
keyAlias =
|
||||||
|
System.getenv()
|
||||||
|
.getOrDefault(
|
||||||
Constants.KEY_ALIAS_VAR,
|
Constants.KEY_ALIAS_VAR,
|
||||||
properties.getProperty(Constants.KEY_ALIAS_VAR)
|
properties.getProperty(Constants.KEY_ALIAS_VAR),
|
||||||
)
|
)
|
||||||
keyPassword = System.getenv().getOrDefault(
|
keyPassword =
|
||||||
|
System.getenv()
|
||||||
|
.getOrDefault(
|
||||||
Constants.KEY_PASS_VAR,
|
Constants.KEY_PASS_VAR,
|
||||||
properties.getProperty(Constants.KEY_PASS_VAR)
|
properties.getProperty(Constants.KEY_PASS_VAR),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,7 +77,7 @@ android {
|
||||||
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,9 +14,10 @@ class MigrationTest {
|
||||||
private val dbName = "migration-test"
|
private val dbName = "migration-test"
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val helper: MigrationTestHelper = MigrationTestHelper(
|
val helper: MigrationTestHelper =
|
||||||
|
MigrationTestHelper(
|
||||||
InstrumentationRegistry.getInstrumentation(),
|
InstrumentationRegistry.getInstrumentation(),
|
||||||
AppDatabase::class.java
|
AppDatabase::class.java,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -50,11 +51,10 @@ class MigrationTest {
|
||||||
"'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,36 +1,42 @@
|
||||||
<?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"
|
||||||
|
@ -38,19 +44,20 @@
|
||||||
<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(
|
[
|
||||||
|
AutoMigration(from = 1, to = 2),
|
||||||
|
AutoMigration(from = 2, to = 3),
|
||||||
|
AutoMigration(
|
||||||
from = 3,
|
from = 3,
|
||||||
to = 4
|
to = 4,
|
||||||
),AutoMigration(
|
),
|
||||||
|
AutoMigration(
|
||||||
from = 4,
|
from = 4,
|
||||||
to = 5
|
to = 5,
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
exportSchema = true
|
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,12 +12,14 @@ 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() {
|
||||||
|
@ -24,16 +27,12 @@ class DataStoreManager(private val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
||||||
}
|
}
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,13 +16,11 @@ 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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,9 +49,10 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
@Inject lateinit var vpnService: VpnService
|
@Inject lateinit var vpnService: VpnService
|
||||||
|
|
||||||
private val networkEventsFlow = MutableStateFlow(WatcherState())
|
private val networkEventsFlow = MutableStateFlow(WatcherState())
|
||||||
|
|
||||||
data class WatcherState(
|
data class WatcherState(
|
||||||
val isWifiConnected: Boolean = false,
|
val isWifiConnected: Boolean = false,
|
||||||
val isVpnConnected : Boolean = false,
|
val isVpnConnected: Boolean = false,
|
||||||
val isEthernetConnected: Boolean = false,
|
val isEthernetConnected: Boolean = false,
|
||||||
val isMobileDataConnected: Boolean = false,
|
val isMobileDataConnected: Boolean = false,
|
||||||
val currentNetworkSSID: String = "",
|
val currentNetworkSSID: String = "",
|
||||||
|
@ -67,7 +68,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
if(settingsRepository.getSettings().isAutoTunnelPaused) {
|
if (settingsRepository.getSettings().isAutoTunnelPaused) {
|
||||||
launchWatcherPausedNotification()
|
launchWatcherPausedNotification()
|
||||||
} else launchWatcherNotification()
|
} else launchWatcherNotification()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -99,15 +100,22 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchWatcherNotification(description: String = getString(R.string.watcher_notification_text_active)) {
|
private fun launchWatcherNotification(
|
||||||
|
description: String = getString(R.string.watcher_notification_text_active)
|
||||||
|
) {
|
||||||
val notification =
|
val notification =
|
||||||
notificationService.createNotification(
|
notificationService.createNotification(
|
||||||
channelId = getString(R.string.watcher_channel_id),
|
channelId = getString(R.string.watcher_channel_id),
|
||||||
channelName = getString(R.string.watcher_channel_name),
|
channelName = getString(R.string.watcher_channel_name),
|
||||||
title = getString(R.string.auto_tunnel_title),
|
title = getString(R.string.auto_tunnel_title),
|
||||||
description = description)
|
description = description,
|
||||||
|
)
|
||||||
ServiceCompat.startForeground(
|
ServiceCompat.startForeground(
|
||||||
this, foregroundId, notification, Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID)
|
this,
|
||||||
|
foregroundId,
|
||||||
|
notification,
|
||||||
|
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchWatcherPausedNotification() {
|
private fun launchWatcherPausedNotification() {
|
||||||
|
@ -124,14 +132,16 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
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)
|
applicationContext.getSystemService(Context.ALARM_SERVICE)
|
||||||
val alarmService: AlarmManager =
|
val alarmService: AlarmManager =
|
||||||
applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||||
alarmService.set(
|
alarmService.set(
|
||||||
AlarmManager.ELAPSED_REALTIME,
|
AlarmManager.ELAPSED_REALTIME,
|
||||||
SystemClock.elapsedRealtime() + 1000,
|
SystemClock.elapsedRealtime() + 1000,
|
||||||
restartServicePendingIntent)
|
restartServicePendingIntent,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun initWakeLock() {
|
private suspend fun initWakeLock() {
|
||||||
|
@ -199,47 +209,56 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
when (it) {
|
when (it) {
|
||||||
is NetworkStatus.Available -> {
|
is NetworkStatus.Available -> {
|
||||||
Timber.d("Gained Mobile data connection")
|
Timber.d("Gained Mobile data connection")
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
networkEventsFlow.value =
|
||||||
isMobileDataConnected = true
|
networkEventsFlow.value.copy(
|
||||||
|
isMobileDataConnected = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
networkEventsFlow.value =
|
||||||
isMobileDataConnected = true
|
networkEventsFlow.value.copy(
|
||||||
|
isMobileDataConnected = true,
|
||||||
)
|
)
|
||||||
Timber.d("Mobile data capabilities changed")
|
Timber.d("Mobile data capabilities changed")
|
||||||
}
|
}
|
||||||
is NetworkStatus.Unavailable -> {
|
is NetworkStatus.Unavailable -> {
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
networkEventsFlow.value =
|
||||||
isMobileDataConnected = false
|
networkEventsFlow.value.copy(
|
||||||
|
isMobileDataConnected = false,
|
||||||
)
|
)
|
||||||
Timber.d("Lost mobile data connection")
|
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(
|
||||||
|
isVpnConnected = false,
|
||||||
)
|
)
|
||||||
Tunnel.State.UP -> networkEventsFlow.value = networkEventsFlow.value.copy(
|
Tunnel.State.UP ->
|
||||||
isVpnConnected = true
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isVpnConnected = true,
|
||||||
)
|
)
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
|
@ -251,19 +270,22 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
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 -> {
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
Timber.d("Ethernet capabilities changed")
|
Timber.d("Ethernet capabilities changed")
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
networkEventsFlow.value =
|
||||||
isEthernetConnected = true
|
networkEventsFlow.value.copy(
|
||||||
|
isEthernetConnected = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is NetworkStatus.Unavailable -> {
|
is NetworkStatus.Unavailable -> {
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
networkEventsFlow.value =
|
||||||
isEthernetConnected = false
|
networkEventsFlow.value.copy(
|
||||||
|
isEthernetConnected = false,
|
||||||
)
|
)
|
||||||
Timber.d("Lost Ethernet connection")
|
Timber.d("Lost Ethernet connection")
|
||||||
}
|
}
|
||||||
|
@ -276,24 +298,28 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
when (it) {
|
when (it) {
|
||||||
is NetworkStatus.Available -> {
|
is NetworkStatus.Available -> {
|
||||||
Timber.d("Gained Wi-Fi connection")
|
Timber.d("Gained Wi-Fi connection")
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
networkEventsFlow.value =
|
||||||
isWifiConnected = true
|
networkEventsFlow.value.copy(
|
||||||
|
isWifiConnected = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
Timber.d("Wifi capabilities changed")
|
Timber.d("Wifi capabilities changed")
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
networkEventsFlow.value =
|
||||||
isWifiConnected = true
|
networkEventsFlow.value.copy(
|
||||||
|
isWifiConnected = true,
|
||||||
)
|
)
|
||||||
val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: ""
|
val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: ""
|
||||||
Timber.d("Detected SSID: $ssid")
|
Timber.d("Detected SSID: $ssid")
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
networkEventsFlow.value =
|
||||||
currentNetworkSSID = ssid
|
networkEventsFlow.value.copy(
|
||||||
|
currentNetworkSSID = ssid,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is NetworkStatus.Unavailable -> {
|
is NetworkStatus.Unavailable -> {
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
networkEventsFlow.value =
|
||||||
isWifiConnected = false
|
networkEventsFlow.value.copy(
|
||||||
|
isWifiConnected = false,
|
||||||
)
|
)
|
||||||
Timber.d("Lost Wi-Fi connection")
|
Timber.d("Lost Wi-Fi connection")
|
||||||
}
|
}
|
||||||
|
@ -301,7 +327,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO clean this up
|
// TODO clean this up
|
||||||
private suspend fun manageVpn() {
|
private suspend fun manageVpn() {
|
||||||
networkEventsFlow.collectLatest {
|
networkEventsFlow.collectLatest {
|
||||||
Timber.i("New watcher state: $it")
|
Timber.i("New watcher state: $it")
|
||||||
|
@ -338,7 +364,8 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
Timber.i("Condition 4 met")
|
Timber.i("Condition 4 met")
|
||||||
}
|
}
|
||||||
(!it.isEthernetConnected &&
|
(!it.isEthernetConnected &&
|
||||||
(it.isWifiConnected && it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) &&
|
(it.isWifiConnected &&
|
||||||
|
it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) &&
|
||||||
(it.isVpnConnected)) -> {
|
(it.isVpnConnected)) -> {
|
||||||
ServiceManager.stopVpnService(this)
|
ServiceManager.stopVpnService(this)
|
||||||
Timber.i("Condition 5 met")
|
Timber.i("Condition 5 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 =
|
||||||
|
if (settings.defaultTunnel != null) {
|
||||||
TunnelConfig.from(settings.defaultTunnel!!)
|
TunnelConfig.from(settings.defaultTunnel!!)
|
||||||
} else if(tunnels.isNotEmpty()) {
|
} else if (tunnels.isNotEmpty()) {
|
||||||
tunnels.first()
|
tunnels.first()
|
||||||
} else {
|
} else {
|
||||||
null
|
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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,20 +158,20 @@ class WireGuardTunnelService : ForegroundService() {
|
||||||
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,13 +24,13 @@ 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 : ConnectivityManager.NetworkCallback(
|
object :
|
||||||
FLAG_INCLUDE_LOCATION_INFO
|
ConnectivityManager.NetworkCallback(
|
||||||
|
FLAG_INCLUDE_LOCATION_INFO,
|
||||||
) {
|
) {
|
||||||
override fun onAvailable(network: Network) {
|
override fun onAvailable(network: Network) {
|
||||||
trySend(NetworkStatus.Available(network))
|
trySend(NetworkStatus.Available(network))
|
||||||
|
@ -47,13 +47,12 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
||||||
trySend(
|
trySend(
|
||||||
NetworkStatus.CapabilitiesChanged(
|
NetworkStatus.CapabilitiesChanged(
|
||||||
network,
|
network,
|
||||||
networkCapabilities
|
networkCapabilities,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
object : ConnectivityManager.NetworkCallback() {
|
object : ConnectivityManager.NetworkCallback() {
|
||||||
override fun onAvailable(network: Network) {
|
override fun onAvailable(network: Network) {
|
||||||
|
@ -71,8 +70,8 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
||||||
trySend(
|
trySend(
|
||||||
NetworkStatus.CapabilitiesChanged(
|
NetworkStatus.CapabilitiesChanged(
|
||||||
network,
|
network,
|
||||||
networkCapabilities
|
networkCapabilities,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -86,9 +85,7 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
||||||
.build()
|
.build()
|
||||||
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
|
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
|
||||||
|
|
||||||
awaitClose {
|
awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) }
|
||||||
connectivityManager.unregisterNetworkCallback(networkStatusCallback)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? {
|
override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? {
|
||||||
|
@ -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
|
|
||||||
): Flow<Result> =
|
|
||||||
map { status ->
|
|
||||||
when (status) {
|
when (status) {
|
||||||
is NetworkStatus.Unavailable -> onUnavailable(status.network)
|
is NetworkStatus.Unavailable -> onUnavailable(status.network)
|
||||||
is NetworkStatus.Available -> onAvailable(status.network)
|
is NetworkStatus.Available -> onAvailable(status.network)
|
||||||
is NetworkStatus.CapabilitiesChanged -> onCapabilitiesChanged(
|
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,22 +13,20 @@ 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(
|
private val tunnelBuilder: NotificationCompat.Builder =
|
||||||
|
NotificationCompat.Builder(
|
||||||
context,
|
context,
|
||||||
context.getString(R.string.vpn_channel_id)
|
context.getString(R.string.vpn_channel_id),
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun createNotification(
|
override fun createNotification(
|
||||||
|
@ -49,13 +47,14 @@ constructor(
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
channelId,
|
channelId,
|
||||||
channelName,
|
channelName,
|
||||||
importance
|
importance,
|
||||||
).let {
|
)
|
||||||
|
.let {
|
||||||
it.description = title
|
it.description = title
|
||||||
it.enableLights(lights)
|
it.enableLights(lights)
|
||||||
it.lightColor = Color.RED
|
it.lightColor = Color.RED
|
||||||
it.enableVibration(vibration)
|
it.enableVibration(vibration)
|
||||||
it.vibrationPattern = longArrayOf(100,200,300)
|
it.vibrationPattern = longArrayOf(100, 200, 300)
|
||||||
it
|
it
|
||||||
}
|
}
|
||||||
notificationManager.createNotificationChannel(channel)
|
notificationManager.createNotificationChannel(channel)
|
||||||
|
@ -65,17 +64,18 @@ constructor(
|
||||||
context,
|
context,
|
||||||
0,
|
0,
|
||||||
notificationIntent,
|
notificationIntent,
|
||||||
PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_IMMUTABLE,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val builder = when(channelId) {
|
val builder =
|
||||||
|
when (channelId) {
|
||||||
context.getString(R.string.watcher_channel_id) -> watcherBuilder
|
context.getString(R.string.watcher_channel_id) -> watcherBuilder
|
||||||
context.getString(R.string.vpn_channel_id) -> tunnelBuilder
|
context.getString(R.string.vpn_channel_id) -> tunnelBuilder
|
||||||
else -> {
|
else -> {
|
||||||
NotificationCompat.Builder(
|
NotificationCompat.Builder(
|
||||||
context,
|
context,
|
||||||
channelId
|
channelId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,8 +83,7 @@ constructor(
|
||||||
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,12 +62,14 @@ 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(
|
||||||
)
|
|
||||||
Action.START.name -> ServiceManager.startVpnServiceForeground(
|
|
||||||
this@ShortcutsActivity,
|
this@ShortcutsActivity,
|
||||||
tunnelConfig.toString()
|
)
|
||||||
|
Action.START.name ->
|
||||||
|
ServiceManager.startVpnServiceForeground(
|
||||||
|
this@ShortcutsActivity,
|
||||||
|
tunnelConfig.toString(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
|
@ -20,35 +20,33 @@ 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 =
|
||||||
|
it.name.ifBlank {
|
||||||
val settings = settingsRepository.getSettings()
|
val settings = settingsRepository.getSettings()
|
||||||
if (settings.defaultTunnel != null) {
|
if (settings.defaultTunnel != null) {
|
||||||
TunnelConfig.from(settings.defaultTunnel!!).name
|
TunnelConfig.from(settings.defaultTunnel!!).name
|
||||||
|
@ -58,6 +56,7 @@ class TunnelControlTile() : TileService() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
{
|
||||||
|
BottomNavBar(
|
||||||
|
navController,
|
||||||
|
listOf(
|
||||||
Screen.Main.navItem,
|
Screen.Main.navItem,
|
||||||
Screen.Settings.navItem,
|
Screen.Settings.navItem,
|
||||||
Screen.Support.navItem)) }
|
Screen.Support.navItem,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} 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 =
|
||||||
|
BottomNavItem(
|
||||||
name = "Tunnels",
|
name = "Tunnels",
|
||||||
route = route,
|
route = route,
|
||||||
icon = Icons.Rounded.Home
|
icon = Icons.Rounded.Home,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
data object Settings: Screen("settings") {
|
|
||||||
val navItem = BottomNavItem(
|
data object Settings : Screen("settings") {
|
||||||
|
val navItem =
|
||||||
|
BottomNavItem(
|
||||||
name = "Settings",
|
name = "Settings",
|
||||||
route = route,
|
route = route,
|
||||||
icon = Icons.Rounded.Settings
|
icon = Icons.Rounded.Settings,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
data object Support: Screen("support") {
|
|
||||||
val navItem = BottomNavItem(
|
data object Support : Screen("support") {
|
||||||
|
val navItem =
|
||||||
|
BottomNavItem(
|
||||||
name = "Support",
|
name = "Support",
|
||||||
route = route,
|
route = route,
|
||||||
icon = Icons.Rounded.QuestionMark
|
icon = Icons.Rounded.QuestionMark,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
data object Config : Screen("config")
|
|
||||||
|
|
||||||
|
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))
|
||||||
|
@ -35,7 +35,7 @@ fun ClickableIconButton(
|
||||||
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() },
|
||||||
onClick()
|
onLongClick = { onHold() },
|
||||||
},
|
),
|
||||||
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,7 +56,7 @@ 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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,15 +66,14 @@ fun SearchBar(onQuery: (queryString: String) -> Unit) {
|
||||||
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,47 +11,36 @@ 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 -> {
|
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
|
||||||
onError("Biometrics not created")
|
onError("Biometrics not created")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
|
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
|
||||||
onError("Biometric hardware not found")
|
onError("Biometric hardware not found")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
|
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
|
||||||
onError("Biometric security update required")
|
onError("Biometric security update required")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
|
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
|
||||||
onError("Biometrics not supported")
|
onError("Biometrics not supported")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
|
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
|
||||||
onError("Biometrics status unknown")
|
onError("Biometrics status unknown")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_SUCCESS -> true
|
BiometricManager.BIOMETRIC_SUCCESS -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,24 +38,24 @@ fun CustomSnackBar(
|
||||||
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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,8 @@ 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(),
|
||||||
|
@ -106,12 +107,10 @@ fun ConfigScreen(
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
@ -124,8 +123,7 @@ fun ConfigScreen(
|
||||||
|
|
||||||
val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() })
|
val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() })
|
||||||
|
|
||||||
val keyboardOptions =
|
val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done)
|
||||||
KeyboardOptions(imeAction = ImeAction.Done)
|
|
||||||
|
|
||||||
val fillMaxHeight = .85f
|
val fillMaxHeight = .85f
|
||||||
val fillMaxWidth = .85f
|
val fillMaxWidth = .85f
|
||||||
|
@ -136,7 +134,8 @@ fun ConfigScreen(
|
||||||
if (uiState.isAllApplicationsEnabled) {
|
if (uiState.isAllApplicationsEnabled) {
|
||||||
"all"
|
"all"
|
||||||
} else {
|
} else {
|
||||||
"${uiState.checkedPackageNames.size} " + (if (uiState.include) "included" else "excluded")
|
"${uiState.checkedPackageNames.size} " +
|
||||||
|
(if (uiState.include) "included" else "excluded")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,12 +152,15 @@ fun ConfigScreen(
|
||||||
onFailure = {
|
onFailure = {
|
||||||
showAuthPrompt = false
|
showAuthPrompt = false
|
||||||
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||||
})
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showApplicationsDialog) {
|
if (showApplicationsDialog) {
|
||||||
val sortedPackages =
|
val sortedPackages =
|
||||||
remember(uiState.packages) { uiState.packages.sortedBy { viewModel.getPackageLabel(it) } }
|
remember(uiState.packages) {
|
||||||
|
uiState.packages.sortedBy { viewModel.getPackageLabel(it) }
|
||||||
|
}
|
||||||
AlertDialog(onDismissRequest = { showApplicationsDialog = false }) {
|
AlertDialog(onDismissRequest = { showApplicationsDialog = false }) {
|
||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
|
@ -167,55 +169,75 @@ fun ConfigScreen(
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth()
|
Modifier.fillMaxWidth()
|
||||||
.fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f)) {
|
.fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f),
|
||||||
|
) {
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp),
|
modifier =
|
||||||
|
Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
Text(stringResource(id = R.string.tunnel_all))
|
Text(stringResource(id = R.string.tunnel_all))
|
||||||
Switch(
|
Switch(
|
||||||
checked = uiState.isAllApplicationsEnabled,
|
checked = uiState.isAllApplicationsEnabled,
|
||||||
onCheckedChange = { viewModel.onAllApplicationsChange(it) })
|
onCheckedChange = { viewModel.onAllApplicationsChange(it) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (!uiState.isAllApplicationsEnabled) {
|
if (!uiState.isAllApplicationsEnabled) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp),
|
modifier =
|
||||||
|
Modifier.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp, vertical = 7.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
Text(stringResource(id = R.string.include))
|
Text(stringResource(id = R.string.include))
|
||||||
Checkbox(
|
Checkbox(
|
||||||
checked = uiState.include,
|
checked = uiState.include,
|
||||||
onCheckedChange = { viewModel.onIncludeChange(!uiState.include) })
|
onCheckedChange = {
|
||||||
|
viewModel.onIncludeChange(!uiState.include)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
Text(stringResource(id = R.string.exclude))
|
Text(stringResource(id = R.string.exclude))
|
||||||
Checkbox(
|
Checkbox(
|
||||||
checked = !uiState.include,
|
checked = !uiState.include,
|
||||||
onCheckedChange = { viewModel.onIncludeChange(!uiState.include) })
|
onCheckedChange = {
|
||||||
|
viewModel.onIncludeChange(!uiState.include)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp),
|
modifier =
|
||||||
|
Modifier.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp, vertical = 7.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
SearchBar(viewModel::emitQueriedPackages)
|
SearchBar(viewModel::emitQueriedPackages)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.padding(5.dp))
|
Spacer(Modifier.padding(5.dp))
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier.fillMaxHeight(4 / 5f)) {
|
modifier = Modifier.fillMaxHeight(4 / 5f),
|
||||||
|
) {
|
||||||
items(sortedPackages, key = { it.packageName }) { pack ->
|
items(sortedPackages, key = { it.packageName }) { pack ->
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
modifier = Modifier.fillMaxSize().padding(5.dp)) {
|
modifier = Modifier.fillMaxSize().padding(5.dp),
|
||||||
|
) {
|
||||||
Row(modifier = Modifier.fillMaxWidth(fillMaxWidth)) {
|
Row(modifier = Modifier.fillMaxWidth(fillMaxWidth)) {
|
||||||
val drawable =
|
val drawable =
|
||||||
pack.applicationInfo?.loadIcon(context.packageManager)
|
pack.applicationInfo?.loadIcon(context.packageManager)
|
||||||
|
@ -223,28 +245,34 @@ fun ConfigScreen(
|
||||||
Image(
|
Image(
|
||||||
painter = DrawablePainter(drawable),
|
painter = DrawablePainter(drawable),
|
||||||
stringResource(id = R.string.icon),
|
stringResource(id = R.string.icon),
|
||||||
modifier = Modifier.size(50.dp, 50.dp))
|
modifier = Modifier.size(50.dp, 50.dp),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Android,
|
Icons.Rounded.Android,
|
||||||
stringResource(id = R.string.edit),
|
stringResource(id = R.string.edit),
|
||||||
modifier = Modifier.size(50.dp, 50.dp))
|
modifier = Modifier.size(50.dp, 50.dp),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
viewModel.getPackageLabel(pack),
|
viewModel.getPackageLabel(pack),
|
||||||
modifier = Modifier.padding(5.dp))
|
modifier = Modifier.padding(5.dp),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Checkbox(
|
Checkbox(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
checked =
|
checked =
|
||||||
(uiState.checkedPackageNames.contains(pack.packageName)),
|
(uiState.checkedPackageNames.contains(
|
||||||
|
pack.packageName
|
||||||
|
)),
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
if (it) {
|
if (it) {
|
||||||
viewModel.onAddCheckedPackage(pack.packageName)
|
viewModel.onAddCheckedPackage(pack.packageName)
|
||||||
} else {
|
} else {
|
||||||
viewModel.onRemoveCheckedPackage(pack.packageName)
|
viewModel.onRemoveCheckedPackage(pack.packageName)
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -252,7 +280,8 @@ fun ConfigScreen(
|
||||||
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))
|
||||||
}
|
}
|
||||||
|
@ -287,19 +316,23 @@ fun ConfigScreen(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
containerColor = fobColor,
|
containerColor = fobColor,
|
||||||
shape = RoundedCornerShape(16.dp)) {
|
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(
|
Surface(
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
shadowElevation = 2.dp,
|
shadowElevation = 2.dp,
|
||||||
|
@ -311,57 +344,55 @@ fun ConfigScreen(
|
||||||
} else {
|
} else {
|
||||||
Modifier.fillMaxWidth(fillMaxWidth)
|
Modifier.fillMaxWidth(fillMaxWidth)
|
||||||
})
|
})
|
||||||
.padding(top = 50.dp, bottom = 10.dp)) {
|
.padding(top = 50.dp, bottom = 10.dp),
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier.padding(15.dp).focusGroup()) {
|
modifier = Modifier.padding(15.dp).focusGroup(),
|
||||||
|
) {
|
||||||
SectionTitle(
|
SectionTitle(
|
||||||
stringResource(R.string.interface_), padding = screenPadding)
|
stringResource(R.string.interface_),
|
||||||
|
padding = screenPadding,
|
||||||
|
)
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = uiState.tunnelName,
|
value = uiState.tunnelName,
|
||||||
onValueChange = { value -> viewModel.onTunnelNameChange(value) },
|
onValueChange = { value -> viewModel.onTunnelNameChange(value) },
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.name),
|
label = stringResource(R.string.name),
|
||||||
hint = stringResource(R.string.tunnel_name).lowercase(),
|
hint = stringResource(R.string.tunnel_name).lowercase(),
|
||||||
modifier =
|
modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
|
||||||
Modifier
|
)
|
||||||
.fillMaxWidth()
|
|
||||||
.focusRequester(focusRequester))
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier =
|
modifier = Modifier.fillMaxWidth().clickable { showAuthPrompt = true },
|
||||||
Modifier.fillMaxWidth().clickable {
|
|
||||||
showAuthPrompt = true
|
|
||||||
},
|
|
||||||
value = uiState.interfaceProxy.privateKey,
|
value = uiState.interfaceProxy.privateKey,
|
||||||
visualTransformation =
|
visualTransformation =
|
||||||
if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) ||
|
if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated)
|
||||||
isAuthenticated)
|
|
||||||
VisualTransformation.None
|
VisualTransformation.None
|
||||||
else PasswordVisualTransformation(),
|
else PasswordVisualTransformation(),
|
||||||
enabled =
|
enabled = (id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
|
||||||
(id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
|
|
||||||
onValueChange = { value -> viewModel.onPrivateKeyChange(value) },
|
onValueChange = { value -> viewModel.onPrivateKeyChange(value) },
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.focusRequester(FocusRequester.Default),
|
modifier = Modifier.focusRequester(FocusRequester.Default),
|
||||||
onClick = { viewModel.generateKeyPair() }) {
|
onClick = { viewModel.generateKeyPair() },
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Refresh,
|
Icons.Rounded.Refresh,
|
||||||
stringResource(R.string.rotate_keys),
|
stringResource(R.string.rotate_keys),
|
||||||
tint = Color.White)
|
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()
|
|
||||||
.focusRequester(FocusRequester.Default),
|
|
||||||
value = uiState.interfaceProxy.publicKey,
|
value = uiState.interfaceProxy.publicKey,
|
||||||
enabled = false,
|
enabled = false,
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
|
@ -370,67 +401,64 @@ fun ConfigScreen(
|
||||||
modifier = Modifier.focusRequester(FocusRequester.Default),
|
modifier = Modifier.focusRequester(FocusRequester.Default),
|
||||||
onClick = {
|
onClick = {
|
||||||
clipboardManager.setText(
|
clipboardManager.setText(
|
||||||
AnnotatedString(uiState.interfaceProxy.publicKey))
|
AnnotatedString(uiState.interfaceProxy.publicKey),
|
||||||
}) {
|
)
|
||||||
|
},
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.ContentCopy,
|
Icons.Rounded.ContentCopy,
|
||||||
stringResource(R.string.copy_public_key),
|
stringResource(R.string.copy_public_key),
|
||||||
tint = Color.White)
|
tint = Color.White,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
label = { Text(stringResource(R.string.public_key)) },
|
label = { Text(stringResource(R.string.public_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,
|
||||||
|
)
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = uiState.interfaceProxy.addresses,
|
value = uiState.interfaceProxy.addresses,
|
||||||
onValueChange = { value ->
|
onValueChange = { value -> viewModel.onAddressesChanged(value) },
|
||||||
viewModel.onAddressesChanged(value)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.addresses),
|
label = stringResource(R.string.addresses),
|
||||||
hint = stringResource(R.string.comma_separated_list),
|
hint = stringResource(R.string.comma_separated_list),
|
||||||
modifier =
|
modifier = Modifier.fillMaxWidth(3 / 5f).padding(end = 5.dp),
|
||||||
Modifier
|
)
|
||||||
.fillMaxWidth(3 / 5f)
|
|
||||||
.padding(end = 5.dp))
|
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = uiState.interfaceProxy.listenPort,
|
value = uiState.interfaceProxy.listenPort,
|
||||||
onValueChange = { value ->
|
onValueChange = { value -> viewModel.onListenPortChanged(value) },
|
||||||
viewModel.onListenPortChanged(value)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.listen_port),
|
label = stringResource(R.string.listen_port),
|
||||||
hint = stringResource(R.string.random),
|
hint = stringResource(R.string.random),
|
||||||
modifier = Modifier.width(IntrinsicSize.Min))
|
modifier = Modifier.width(IntrinsicSize.Min),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = uiState.interfaceProxy.dnsServers,
|
value = uiState.interfaceProxy.dnsServers,
|
||||||
onValueChange = { value ->
|
onValueChange = { value -> viewModel.onDnsServersChanged(value) },
|
||||||
viewModel.onDnsServersChanged(value)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.dns_servers),
|
label = stringResource(R.string.dns_servers),
|
||||||
hint = stringResource(R.string.comma_separated_list),
|
hint = stringResource(R.string.comma_separated_list),
|
||||||
modifier =
|
modifier = Modifier.fillMaxWidth(3 / 5f).padding(end = 5.dp),
|
||||||
Modifier
|
)
|
||||||
.fillMaxWidth(3 / 5f)
|
|
||||||
.padding(end = 5.dp))
|
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = uiState.interfaceProxy.mtu,
|
value = uiState.interfaceProxy.mtu,
|
||||||
onValueChange = { value -> viewModel.onMtuChanged(value) },
|
onValueChange = { value -> viewModel.onMtuChanged(value) },
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.mtu),
|
label = stringResource(R.string.mtu),
|
||||||
hint = stringResource(R.string.auto),
|
hint = stringResource(R.string.auto),
|
||||||
modifier = Modifier.width(IntrinsicSize.Min))
|
modifier = Modifier.width(IntrinsicSize.Min),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
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 = true }) {
|
TextButton(onClick = { showApplicationsDialog = true }) {
|
||||||
Text(applicationButtonText())
|
Text(applicationButtonText())
|
||||||
}
|
}
|
||||||
|
@ -449,18 +477,22 @@ fun ConfigScreen(
|
||||||
} else {
|
} else {
|
||||||
Modifier.fillMaxWidth(fillMaxWidth)
|
Modifier.fillMaxWidth(fillMaxWidth)
|
||||||
})
|
})
|
||||||
.padding(top = 10.dp, bottom = 10.dp)) {
|
.padding(top = 10.dp, bottom = 10.dp),
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier =
|
modifier = Modifier.padding(horizontal = 15.dp).padding(bottom = 10.dp),
|
||||||
Modifier.padding(horizontal = 15.dp).padding(bottom = 10.dp)) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 5.dp)) {
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 5.dp),
|
||||||
|
) {
|
||||||
SectionTitle(
|
SectionTitle(
|
||||||
stringResource(R.string.peer), padding = screenPadding)
|
stringResource(R.string.peer),
|
||||||
|
padding = screenPadding,
|
||||||
|
)
|
||||||
IconButton(onClick = { viewModel.onDeletePeer(index) }) {
|
IconButton(onClick = { viewModel.onDeletePeer(index) }) {
|
||||||
Icon(Icons.Rounded.Delete, stringResource(R.string.delete))
|
Icon(Icons.Rounded.Delete, stringResource(R.string.delete))
|
||||||
}
|
}
|
||||||
|
@ -474,7 +506,8 @@ fun ConfigScreen(
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.public_key),
|
label = stringResource(R.string.public_key),
|
||||||
hint = stringResource(R.string.base64_key),
|
hint = stringResource(R.string.base64_key),
|
||||||
modifier = Modifier.fillMaxWidth())
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = peer.preSharedKey,
|
value = peer.preSharedKey,
|
||||||
onValueChange = { value ->
|
onValueChange = { value ->
|
||||||
|
@ -483,7 +516,8 @@ fun ConfigScreen(
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.preshared_key),
|
label = stringResource(R.string.preshared_key),
|
||||||
hint = stringResource(R.string.optional),
|
hint = stringResource(R.string.optional),
|
||||||
modifier = Modifier.fillMaxWidth())
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
value = peer.persistentKeepalive,
|
value = peer.persistentKeepalive,
|
||||||
|
@ -494,7 +528,8 @@ fun ConfigScreen(
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.seconds),
|
stringResource(R.string.seconds),
|
||||||
modifier = Modifier.padding(end = 10.dp))
|
modifier = Modifier.padding(end = 10.dp),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
label = { Text(stringResource(R.string.persistent_keepalive)) },
|
label = { Text(stringResource(R.string.persistent_keepalive)) },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
|
@ -502,7 +537,8 @@ fun ConfigScreen(
|
||||||
Text(stringResource(R.string.optional_no_recommend))
|
Text(stringResource(R.string.optional_no_recommend))
|
||||||
},
|
},
|
||||||
keyboardOptions = keyboardOptions,
|
keyboardOptions = keyboardOptions,
|
||||||
keyboardActions = keyboardActions)
|
keyboardActions = keyboardActions,
|
||||||
|
)
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = peer.endpoint,
|
value = peer.endpoint,
|
||||||
onValueChange = { value ->
|
onValueChange = { value ->
|
||||||
|
@ -511,7 +547,8 @@ fun ConfigScreen(
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.endpoint),
|
label = stringResource(R.string.endpoint),
|
||||||
hint = stringResource(R.string.endpoint).lowercase(),
|
hint = stringResource(R.string.endpoint).lowercase(),
|
||||||
modifier = Modifier.fillMaxWidth())
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
value = peer.allowedIps,
|
value = peer.allowedIps,
|
||||||
|
@ -525,17 +562,20 @@ fun ConfigScreen(
|
||||||
Text(stringResource(R.string.comma_separated_list))
|
Text(stringResource(R.string.comma_separated_list))
|
||||||
},
|
},
|
||||||
keyboardOptions = keyboardOptions,
|
keyboardOptions = keyboardOptions,
|
||||||
keyboardActions = keyboardActions)
|
keyboardActions = keyboardActions,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxSize().padding(bottom = 140.dp)) {
|
modifier = Modifier.fillMaxSize().padding(bottom = 140.dp),
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Center) {
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
TextButton(onClick = { viewModel.addEmptyPeer() }) {
|
TextButton(onClick = { viewModel.addEmptyPeer() }) {
|
||||||
Text(stringResource(R.string.add_peer))
|
Text(stringResource(R.string.add_peer))
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ 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 = ""
|
||||||
|
|
|
@ -46,9 +46,11 @@ constructor(
|
||||||
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) =
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val packages = getQueriedPackages("")
|
val packages = getQueriedPackages("")
|
||||||
val state = if(tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
val state =
|
||||||
|
if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
||||||
val tunnelConfig =
|
val tunnelConfig =
|
||||||
tunnelConfigRepository.getAll().firstOrNull { it.id.toString() == tunnelId }
|
tunnelConfigRepository.getAll().firstOrNull { it.id.toString() == tunnelId }
|
||||||
if (tunnelConfig != null) {
|
if (tunnelConfig != null) {
|
||||||
|
@ -76,7 +78,8 @@ constructor(
|
||||||
isAllApplicationsEnabled,
|
isAllApplicationsEnabled,
|
||||||
false,
|
false,
|
||||||
tunnelConfig,
|
tunnelConfig,
|
||||||
tunnelConfig.name)
|
tunnelConfig.name,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
ConfigUiState(loading = false, packages = packages)
|
ConfigUiState(loading = false, packages = packages)
|
||||||
}
|
}
|
||||||
|
@ -85,6 +88,7 @@ constructor(
|
||||||
}
|
}
|
||||||
_uiState.value = state
|
_uiState.value = state
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTunnelNameChange(name: String) {
|
fun onTunnelNameChange(name: String) {
|
||||||
_uiState.value = _uiState.value.copy(tunnelName = name)
|
_uiState.value = _uiState.value.copy(tunnelName = name)
|
||||||
}
|
}
|
||||||
|
@ -95,7 +99,9 @@ constructor(
|
||||||
|
|
||||||
fun onAddCheckedPackage(packageName: String) {
|
fun onAddCheckedPackage(packageName: String) {
|
||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(checkedPackageNames = _uiState.value.checkedPackageNames + packageName)
|
_uiState.value.copy(
|
||||||
|
checkedPackageNames = _uiState.value.checkedPackageNames + packageName
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAllApplicationsChange(isAllApplicationsEnabled: Boolean) {
|
fun onAllApplicationsChange(isAllApplicationsEnabled: Boolean) {
|
||||||
|
@ -104,7 +110,9 @@ constructor(
|
||||||
|
|
||||||
fun onRemoveCheckedPackage(packageName: String) {
|
fun onRemoveCheckedPackage(packageName: String) {
|
||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(checkedPackageNames = _uiState.value.checkedPackageNames - packageName)
|
_uiState.value.copy(
|
||||||
|
checkedPackageNames = _uiState.value.checkedPackageNames - packageName
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getQueriedPackages(query: String): List<PackageInfo> {
|
private fun getQueriedPackages(query: String): List<PackageInfo> {
|
||||||
|
@ -124,7 +132,9 @@ constructor(
|
||||||
private fun getPackagesHoldingPermissions(permissions: Array<String>): List<PackageInfo> {
|
private fun getPackagesHoldingPermissions(permissions: Array<String>): List<PackageInfo> {
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
packageManager.getPackagesHoldingPermissions(
|
packageManager.getPackagesHoldingPermissions(
|
||||||
permissions, PackageManager.PackageInfoFlags.of(0L))
|
permissions,
|
||||||
|
PackageManager.PackageInfoFlags.of(0L),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
packageManager.getPackagesHoldingPermissions(permissions, 0)
|
packageManager.getPackagesHoldingPermissions(permissions, 0)
|
||||||
}
|
}
|
||||||
|
@ -135,9 +145,7 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveConfig(tunnelConfig: TunnelConfig) =
|
private fun saveConfig(tunnelConfig: TunnelConfig) =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch { tunnelConfigRepository.save(tunnelConfig) }
|
||||||
tunnelConfigRepository.save(tunnelConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) =
|
private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
@ -179,7 +187,9 @@ constructor(
|
||||||
val builder = Interface.Builder()
|
val builder = Interface.Builder()
|
||||||
builder.parsePrivateKey(_uiState.value.interfaceProxy.privateKey.trim())
|
builder.parsePrivateKey(_uiState.value.interfaceProxy.privateKey.trim())
|
||||||
builder.parseAddresses(_uiState.value.interfaceProxy.addresses.trim())
|
builder.parseAddresses(_uiState.value.interfaceProxy.addresses.trim())
|
||||||
|
if (_uiState.value.interfaceProxy.dnsServers.isNotEmpty()) {
|
||||||
builder.parseDnsServers(_uiState.value.interfaceProxy.dnsServers.trim())
|
builder.parseDnsServers(_uiState.value.interfaceProxy.dnsServers.trim())
|
||||||
|
}
|
||||||
if (_uiState.value.interfaceProxy.mtu.isNotEmpty())
|
if (_uiState.value.interfaceProxy.mtu.isNotEmpty())
|
||||||
builder.parseMtu(_uiState.value.interfaceProxy.mtu.trim())
|
builder.parseMtu(_uiState.value.interfaceProxy.mtu.trim())
|
||||||
if (_uiState.value.interfaceProxy.listenPort.isNotEmpty()) {
|
if (_uiState.value.interfaceProxy.listenPort.isNotEmpty()) {
|
||||||
|
@ -198,7 +208,9 @@ constructor(
|
||||||
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
||||||
val tunnelConfig =
|
val tunnelConfig =
|
||||||
_uiState.value.tunnel?.copy(
|
_uiState.value.tunnel?.copy(
|
||||||
name = _uiState.value.tunnelName, wgQuick = config.toWgQuickString())
|
name = _uiState.value.tunnelName,
|
||||||
|
wgQuick = config.toWgQuickString(),
|
||||||
|
)
|
||||||
updateTunnelConfig(tunnelConfig)
|
updateTunnelConfig(tunnelConfig)
|
||||||
Result.Success(Event.Message.ConfigSaved)
|
Result.Success(Event.Message.ConfigSaved)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -211,7 +223,10 @@ constructor(
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
proxyPeers =
|
proxyPeers =
|
||||||
_uiState.value.proxyPeers.update(
|
_uiState.value.proxyPeers.update(
|
||||||
index, _uiState.value.proxyPeers[index].copy(publicKey = value)))
|
index,
|
||||||
|
_uiState.value.proxyPeers[index].copy(publicKey = value),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPreSharedKeyChange(index: Int, value: String) {
|
fun onPreSharedKeyChange(index: Int, value: String) {
|
||||||
|
@ -219,7 +234,10 @@ constructor(
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
proxyPeers =
|
proxyPeers =
|
||||||
_uiState.value.proxyPeers.update(
|
_uiState.value.proxyPeers.update(
|
||||||
index, _uiState.value.proxyPeers[index].copy(preSharedKey = value)))
|
index,
|
||||||
|
_uiState.value.proxyPeers[index].copy(preSharedKey = value),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEndpointChange(index: Int, value: String) {
|
fun onEndpointChange(index: Int, value: String) {
|
||||||
|
@ -227,7 +245,10 @@ constructor(
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
proxyPeers =
|
proxyPeers =
|
||||||
_uiState.value.proxyPeers.update(
|
_uiState.value.proxyPeers.update(
|
||||||
index, _uiState.value.proxyPeers[index].copy(endpoint = value)))
|
index,
|
||||||
|
_uiState.value.proxyPeers[index].copy(endpoint = value),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAllowedIpsChange(index: Int, value: String) {
|
fun onAllowedIpsChange(index: Int, value: String) {
|
||||||
|
@ -235,7 +256,10 @@ constructor(
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
proxyPeers =
|
proxyPeers =
|
||||||
_uiState.value.proxyPeers.update(
|
_uiState.value.proxyPeers.update(
|
||||||
index, _uiState.value.proxyPeers[index].copy(allowedIps = value)))
|
index,
|
||||||
|
_uiState.value.proxyPeers[index].copy(allowedIps = value),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPersistentKeepaliveChanged(index: Int, value: String) {
|
fun onPersistentKeepaliveChanged(index: Int, value: String) {
|
||||||
|
@ -243,12 +267,16 @@ constructor(
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
proxyPeers =
|
proxyPeers =
|
||||||
_uiState.value.proxyPeers.update(
|
_uiState.value.proxyPeers.update(
|
||||||
index, _uiState.value.proxyPeers[index].copy(persistentKeepalive = value)))
|
index,
|
||||||
|
_uiState.value.proxyPeers[index].copy(persistentKeepalive = value),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDeletePeer(index: Int) {
|
fun onDeletePeer(index: Int) {
|
||||||
_uiState.value = _uiState.value.copy(
|
_uiState.value =
|
||||||
proxyPeers = _uiState.value.proxyPeers.removeAt(index)
|
_uiState.value.copy(
|
||||||
|
proxyPeers = _uiState.value.proxyPeers.removeAt(index),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,22 +291,30 @@ constructor(
|
||||||
interfaceProxy =
|
interfaceProxy =
|
||||||
_uiState.value.interfaceProxy.copy(
|
_uiState.value.interfaceProxy.copy(
|
||||||
privateKey = keyPair.privateKey.toBase64(),
|
privateKey = keyPair.privateKey.toBase64(),
|
||||||
publicKey = keyPair.publicKey.toBase64()))
|
publicKey = keyPair.publicKey.toBase64(),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAddressesChanged(value: String) {
|
fun onAddressesChanged(value: String) {
|
||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value))
|
_uiState.value.copy(
|
||||||
|
interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onListenPortChanged(value: String) {
|
fun onListenPortChanged(value: String) {
|
||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value))
|
_uiState.value.copy(
|
||||||
|
interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDnsServersChanged(value: String) {
|
fun onDnsServersChanged(value: String) {
|
||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value))
|
_uiState.value.copy(
|
||||||
|
interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onMtuChanged(value: String) {
|
fun onMtuChanged(value: String) {
|
||||||
|
@ -288,12 +324,16 @@ constructor(
|
||||||
|
|
||||||
private fun onInterfacePublicKeyChange(value: String) {
|
private fun onInterfacePublicKeyChange(value: String) {
|
||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value))
|
_uiState.value.copy(
|
||||||
|
interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPrivateKeyChange(value: String) {
|
fun onPrivateKeyChange(value: String) {
|
||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value))
|
_uiState.value.copy(
|
||||||
|
interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value)
|
||||||
|
)
|
||||||
if (NumberUtils.isValidKey(value)) {
|
if (NumberUtils.isValidKey(value)) {
|
||||||
val pair = KeyPair(Key.fromBase64(value))
|
val pair = KeyPair(Key.fromBase64(value))
|
||||||
onInterfacePublicKeyChange(pair.publicKey.toBase64())
|
onInterfacePublicKeyChange(pair.publicKey.toBase64())
|
||||||
|
|
|
@ -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)
|
||||||
|
@ -127,6 +128,7 @@ fun MainScreen(
|
||||||
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 showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
|
||||||
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
@ -155,21 +157,28 @@ fun MainScreen(
|
||||||
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 {
|
} else {
|
||||||
context.packageManager.queryIntentActivities(
|
context.packageManager.queryIntentActivities(
|
||||||
intent, PackageManager.MATCH_DEFAULT_ONLY)
|
intent,
|
||||||
|
PackageManager.MATCH_DEFAULT_ONLY,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (activitiesToResolveIntent.all {
|
if (
|
||||||
|
activitiesToResolveIntent.all {
|
||||||
val name = it.activityInfo.packageName
|
val name = it.activityInfo.packageName
|
||||||
name.startsWith(Constants.GOOGLE_TV_EXPLORER_STUB) ||
|
name.startsWith(Constants.GOOGLE_TV_EXPLORER_STUB) ||
|
||||||
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
|
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
|
||||||
}) {
|
}
|
||||||
|
) {
|
||||||
showSnackbarMessage(Event.Error.FileExplorerRequired.message)
|
showSnackbarMessage(Event.Error.FileExplorerRequired.message)
|
||||||
}
|
}
|
||||||
return intent
|
return intent
|
||||||
}
|
}
|
||||||
}) { data ->
|
},
|
||||||
|
) { data ->
|
||||||
if (data == null) return@rememberLauncherForActivityResult
|
if (data == null) return@rememberLauncherForActivityResult
|
||||||
scope.launch {
|
scope.launch {
|
||||||
viewModel.onTunnelFileSelected(data).let {
|
viewModel.onTunnelFileSelected(data).let {
|
||||||
|
@ -194,7 +203,8 @@ fun MainScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
|
||||||
AnimatedVisibility(showPrimaryChangeAlertDialog) {
|
AnimatedVisibility(showPrimaryChangeAlertDialog) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
|
@ -205,7 +215,8 @@ fun MainScreen(
|
||||||
viewModel.onDefaultTunnelChange(selectedTunnel)
|
viewModel.onDefaultTunnelChange(selectedTunnel)
|
||||||
showPrimaryChangeAlertDialog = false
|
showPrimaryChangeAlertDialog = false
|
||||||
selectedTunnel = null
|
selectedTunnel = null
|
||||||
}) {
|
},
|
||||||
|
) {
|
||||||
Text(text = stringResource(R.string.okay))
|
Text(text = stringResource(R.string.okay))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -215,7 +226,32 @@ fun MainScreen(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
title = { Text(text = stringResource(R.string.primary_tunnel_change)) },
|
title = { Text(text = stringResource(R.string.primary_tunnel_change)) },
|
||||||
text = { Text(text = stringResource(R.string.primary_tunnel_change_question)) })
|
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) {
|
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
|
||||||
|
@ -228,7 +264,8 @@ fun MainScreen(
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onTap = {
|
onTap = {
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) selectedTunnel = null
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) selectedTunnel = null
|
||||||
})
|
},
|
||||||
|
)
|
||||||
},
|
},
|
||||||
floatingActionButtonPosition = FabPosition.End,
|
floatingActionButtonPosition = FabPosition.End,
|
||||||
topBar = {
|
topBar = {
|
||||||
|
@ -238,25 +275,37 @@ fun MainScreen(
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.requiredWidth(LocalConfiguration.current.screenWidthDp.dp).padding(end = 5.dp)) {
|
modifier =
|
||||||
|
Modifier.requiredWidth(LocalConfiguration.current.screenWidthDp.dp)
|
||||||
|
.padding(end = 5.dp),
|
||||||
|
) {
|
||||||
Row {
|
Row {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Bolt,
|
Icons.Rounded.Bolt,
|
||||||
stringResource(id = R.string.auto),
|
stringResource(id = R.string.auto),
|
||||||
modifier = Modifier.size(25.dp),
|
modifier = Modifier.size(25.dp),
|
||||||
tint = if(uiState.settings.isAutoTunnelPaused) Color.Gray else mint)
|
tint =
|
||||||
|
if (uiState.settings.isAutoTunnelPaused) Color.Gray
|
||||||
|
else mint,
|
||||||
|
)
|
||||||
Text(
|
Text(
|
||||||
"Auto-tunneling: ${if(uiState.settings.isAutoTunnelPaused) "paused" else "active" }",
|
"Auto-tunneling: ${if (uiState.settings.isAutoTunnelPaused) "paused" else "active"}",
|
||||||
style = typography.bodyLarge,
|
style = typography.bodyLarge,
|
||||||
modifier = Modifier.padding(start = 10.dp))
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if(uiState.settings.isAutoTunnelPaused) TextButton(
|
if (uiState.settings.isAutoTunnelPaused)
|
||||||
|
TextButton(
|
||||||
onClick = { viewModel.resumeAutoTunneling() },
|
onClick = { viewModel.resumeAutoTunneling() },
|
||||||
modifier = Modifier.padding(end = 10.dp)) {
|
modifier = Modifier.padding(end = 10.dp),
|
||||||
|
) {
|
||||||
Text("Resume")
|
Text("Resume")
|
||||||
} else TextButton(
|
}
|
||||||
|
else
|
||||||
|
TextButton(
|
||||||
onClick = { viewModel.pauseAutoTunneling() },
|
onClick = { viewModel.pauseAutoTunneling() },
|
||||||
modifier = Modifier.padding(end = 10.dp)) {
|
modifier = Modifier.padding(end = 10.dp),
|
||||||
|
) {
|
||||||
Text("Pause")
|
Text("Pause")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -267,14 +316,17 @@ fun MainScreen(
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = isVisible.value,
|
visible = isVisible.value,
|
||||||
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
||||||
exit = slideOutVertically(targetOffsetY = { it * 2 })) {
|
exit = slideOutVertically(targetOffsetY = { it * 2 }),
|
||||||
|
) {
|
||||||
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 =
|
||||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
(if (
|
||||||
uiState.tunnels.isEmpty())
|
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
||||||
|
uiState.tunnels.isEmpty()
|
||||||
|
)
|
||||||
Modifier.focusRequester(focusRequester)
|
Modifier.focusRequester(focusRequester)
|
||||||
else Modifier)
|
else Modifier)
|
||||||
.padding(bottom = 90.dp)
|
.padding(bottom = 90.dp)
|
||||||
|
@ -285,25 +337,31 @@ fun MainScreen(
|
||||||
},
|
},
|
||||||
onClick = { showBottomSheet = true },
|
onClick = { showBottomSheet = true },
|
||||||
containerColor = fobColor,
|
containerColor = fobColor,
|
||||||
shape = RoundedCornerShape(16.dp)) {
|
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 =
|
||||||
|
@ -312,14 +370,17 @@ fun MainScreen(
|
||||||
showBottomSheet = false
|
showBottomSheet = false
|
||||||
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
||||||
}
|
}
|
||||||
.padding(10.dp)) {
|
.padding(10.dp),
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.FileOpen,
|
Icons.Filled.FileOpen,
|
||||||
contentDescription = stringResource(id = R.string.open_file),
|
contentDescription = stringResource(id = R.string.open_file),
|
||||||
modifier = Modifier.padding(10.dp))
|
modifier = Modifier.padding(10.dp),
|
||||||
|
)
|
||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.add_tunnels_text),
|
stringResource(id = R.string.add_tunnels_text),
|
||||||
modifier = Modifier.padding(10.dp))
|
modifier = Modifier.padding(10.dp),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Divider()
|
Divider()
|
||||||
|
@ -332,20 +393,26 @@ fun MainScreen(
|
||||||
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(
|
||||||
|
context.getString(R.string.scanning_qr)
|
||||||
|
)
|
||||||
scanOptions.setBeepEnabled(false)
|
scanOptions.setBeepEnabled(false)
|
||||||
scanOptions.captureActivity = CaptureActivityPortrait::class.java
|
scanOptions.captureActivity =
|
||||||
|
CaptureActivityPortrait::class.java
|
||||||
scanLauncher.launch(scanOptions)
|
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()
|
||||||
|
@ -355,16 +422,20 @@ fun MainScreen(
|
||||||
.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(
|
Icon(
|
||||||
Icons.Filled.Create,
|
Icons.Filled.Create,
|
||||||
contentDescription = stringResource(id = R.string.create_import),
|
contentDescription = stringResource(id = R.string.create_import),
|
||||||
modifier = Modifier.padding(10.dp))
|
modifier = Modifier.padding(10.dp),
|
||||||
|
)
|
||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.create_import),
|
stringResource(id = R.string.create_import),
|
||||||
modifier = Modifier.padding(10.dp))
|
modifier = Modifier.padding(10.dp),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -373,16 +444,24 @@ fun MainScreen(
|
||||||
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(
|
||||||
|
uiState.tunnels,
|
||||||
|
key = { tunnel -> tunnel.id },
|
||||||
|
) { tunnel ->
|
||||||
val leadingIconColor =
|
val leadingIconColor =
|
||||||
(if (uiState.vpnState.name == tunnel.name &&
|
(if (
|
||||||
uiState.vpnState.status == Tunnel.State.UP) {
|
uiState.vpnState.name == tunnel.name &&
|
||||||
|
uiState.vpnState.status == Tunnel.State.UP
|
||||||
|
) {
|
||||||
uiState.vpnState.statistics
|
uiState.vpnState.statistics
|
||||||
?.mapPeerStats()
|
?.mapPeerStats()
|
||||||
?.map { it.value?.handshakeStatus() }
|
?.map { it.value?.handshakeStatus() }
|
||||||
|
@ -408,19 +487,23 @@ fun MainScreen(
|
||||||
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,
|
text = tunnel.name,
|
||||||
onHold = {
|
onHold = {
|
||||||
if ((uiState.vpnState.status == Tunnel.State.UP) &&
|
if (
|
||||||
(tunnel.name == uiState.vpnState.name)) {
|
(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
|
||||||
}
|
}
|
||||||
|
@ -429,8 +512,10 @@ fun MainScreen(
|
||||||
},
|
},
|
||||||
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 &&
|
||||||
|
(uiState.vpnState.name == tunnel.name)
|
||||||
|
) {
|
||||||
expanded.value = !expanded.value
|
expanded.value = !expanded.value
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -441,38 +526,56 @@ fun MainScreen(
|
||||||
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(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (uiState.settings.isAutoTunnelEnabled && !uiState.settings.isAutoTunnelPaused) {
|
if (
|
||||||
|
uiState.settings.isAutoTunnelEnabled &&
|
||||||
|
!uiState.settings.isAutoTunnelPaused
|
||||||
|
) {
|
||||||
showSnackbarMessage(
|
showSnackbarMessage(
|
||||||
Event.Message.AutoTunnelOffAction.message)
|
Event.Message.AutoTunnelOffAction.message,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
showPrimaryChangeAlertDialog = true
|
showPrimaryChangeAlertDialog = true
|
||||||
}
|
}
|
||||||
}) {
|
},
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Star,
|
Icons.Rounded.Star,
|
||||||
stringResource(id = R.string.set_primary))
|
stringResource(id = R.string.set_primary),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (uiState.settings.isAutoTunnelEnabled && uiState.settings.isTunnelConfigDefault(tunnel)
|
if (
|
||||||
&& !uiState.settings.isAutoTunnelPaused) {
|
uiState.settings.isAutoTunnelEnabled &&
|
||||||
|
uiState.settings.isTunnelConfigDefault(
|
||||||
|
tunnel,
|
||||||
|
) &&
|
||||||
|
!uiState.settings.isAutoTunnelPaused
|
||||||
|
) {
|
||||||
showSnackbarMessage(
|
showSnackbarMessage(
|
||||||
Event.Message.AutoTunnelOffAction.message)
|
Event.Message.AutoTunnelOffAction.message,
|
||||||
} else navController.navigate(
|
)
|
||||||
"${Screen.Config.route}/${selectedTunnel?.id}")
|
} 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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -493,7 +596,8 @@ fun MainScreen(
|
||||||
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)) {
|
||||||
|
@ -501,53 +605,73 @@ fun MainScreen(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (uiState.settings.isAutoTunnelEnabled) {
|
if (uiState.settings.isAutoTunnelEnabled) {
|
||||||
showSnackbarMessage(
|
showSnackbarMessage(
|
||||||
Event.Message.AutoTunnelOffAction.message)
|
Event.Message.AutoTunnelOffAction.message,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
selectedTunnel = tunnel
|
selectedTunnel = tunnel
|
||||||
showPrimaryChangeAlertDialog = true
|
showPrimaryChangeAlertDialog = true
|
||||||
}
|
}
|
||||||
}) {
|
},
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Star,
|
Icons.Rounded.Star,
|
||||||
stringResource(id = R.string.set_primary))
|
stringResource(id = R.string.set_primary),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
onClick = {
|
onClick = {
|
||||||
if (uiState.vpnState.status == Tunnel.State.UP &&
|
if (
|
||||||
(uiState.vpnState.name == tunnel.name)) {
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
|
(uiState.vpnState.name == tunnel.name)
|
||||||
|
) {
|
||||||
expanded.value = !expanded.value
|
expanded.value = !expanded.value
|
||||||
} else {
|
} else {
|
||||||
showSnackbarMessage(Event.Message.TunnelOnAction.message)
|
showSnackbarMessage(
|
||||||
|
Event.Message.TunnelOnAction.message
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}) {
|
},
|
||||||
|
) {
|
||||||
Icon(Icons.Rounded.Info, stringResource(R.string.info))
|
Icon(Icons.Rounded.Info, stringResource(R.string.info))
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (uiState.vpnState.status == Tunnel.State.UP &&
|
if (
|
||||||
tunnel.name == uiState.vpnState.name) {
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
tunnel.name == uiState.vpnState.name
|
||||||
|
) {
|
||||||
|
showSnackbarMessage(
|
||||||
|
Event.Message.TunnelOffAction.message
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
"${Screen.Config.route}/${tunnel.id}")
|
"${Screen.Config.route}/${tunnel.id}",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}) {
|
},
|
||||||
|
) {
|
||||||
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
|
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (uiState.vpnState.status == Tunnel.State.UP &&
|
if (
|
||||||
tunnel.name == uiState.vpnState.name) {
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
tunnel.name == uiState.vpnState.name
|
||||||
|
) {
|
||||||
|
showSnackbarMessage(
|
||||||
|
Event.Message.TunnelOffAction.message
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
viewModel.onDelete(tunnel)
|
showDeleteTunnelAlertDialog = true
|
||||||
}
|
}
|
||||||
}) {
|
},
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Delete,
|
Icons.Rounded.Delete,
|
||||||
stringResource(id = R.string.delete))
|
stringResource(id = R.string.delete),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
TunnelSwitch()
|
TunnelSwitch()
|
||||||
}
|
}
|
||||||
|
@ -555,7 +679,8 @@ fun MainScreen(
|
||||||
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
|
||||||
|
@ -58,21 +55,21 @@ constructor(
|
||||||
.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)
|
|
||||||
if (settings.isAutoTunnelEnabled &&
|
|
||||||
watcherState == ServiceState.STOPPED) {
|
|
||||||
ServiceManager.startWatcherService(application.applicationContext)
|
ServiceManager.startWatcherService(application.applicationContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopWatcherService() = viewModelScope.launch(Dispatchers.IO) {
|
private fun stopWatcherService() =
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
ServiceManager.stopWatcherService(application.applicationContext)
|
ServiceManager.stopWatcherService(application.applicationContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDelete(tunnel: TunnelConfig) {
|
fun onDelete(tunnel: TunnelConfig) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
if (tunnelConfigRepository.count() == 1) {
|
if (tunnelConfigRepository.count() == 1) {
|
||||||
|
@ -88,26 +85,28 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTunnelStart(tunnelConfig: TunnelConfig) = viewModelScope.launch(Dispatchers.IO) {
|
fun onTunnelStart(tunnelConfig: TunnelConfig) =
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
Timber.d("On start called!")
|
||||||
stopActiveTunnel().await()
|
stopActiveTunnel().await()
|
||||||
startTunnel(tunnelConfig)
|
startTunnel(tunnelConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch(Dispatchers.IO) {
|
private fun startTunnel(tunnelConfig: TunnelConfig) =
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
Timber.d("Start tunnel via manager")
|
||||||
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
|
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopActiveTunnel() =
|
private fun stopActiveTunnel() =
|
||||||
viewModelScope.async(Dispatchers.IO) {
|
viewModelScope.async(Dispatchers.IO) {
|
||||||
if (ServiceManager.getServiceState(
|
|
||||||
application.applicationContext, WireGuardTunnelService::class.java) ==
|
|
||||||
ServiceState.STARTED) {
|
|
||||||
onTunnelStop()
|
onTunnelStop()
|
||||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun onTunnelStop() = viewModelScope.launch(Dispatchers.IO) {
|
fun onTunnelStop() =
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
Timber.d("Stopping active tunnel")
|
||||||
ServiceManager.stopVpnService(application.applicationContext)
|
ServiceManager.stopVpnService(application.applicationContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,7 +114,7 @@ constructor(
|
||||||
TunnelConfig.configFromQuick(config)
|
TunnelConfig.configFromQuick(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onTunnelQrResult(result: String) : Result<Unit> {
|
suspend fun onTunnelQrResult(result: String): Result<Unit> {
|
||||||
return try {
|
return try {
|
||||||
validateConfigString(result)
|
validateConfigString(result)
|
||||||
val tunnelConfig =
|
val tunnelConfig =
|
||||||
|
@ -139,13 +138,14 @@ constructor(
|
||||||
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 {
|
||||||
|
when (it) {
|
||||||
is Result.Error -> return Result.Error(Event.Error.FileReadFailed)
|
is Result.Error -> return Result.Error(Event.Error.FileReadFailed)
|
||||||
is Result.Success -> return it
|
is Result.Success -> return it
|
||||||
}
|
}
|
||||||
|
@ -166,7 +166,8 @@ constructor(
|
||||||
ZipInputStream(getInputStreamFromUri(uri)).use { zip ->
|
ZipInputStream(getInputStreamFromUri(uri)).use { zip ->
|
||||||
generateSequence { zip.nextEntry }
|
generateSequence { zip.nextEntry }
|
||||||
.filterNot {
|
.filterNot {
|
||||||
it.isDirectory || getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
|
it.isDirectory ||
|
||||||
|
getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
|
||||||
}
|
}
|
||||||
.forEach {
|
.forEach {
|
||||||
val name = getNameFromFileName(it.name)
|
val name = getNameFromFileName(it.name)
|
||||||
|
@ -178,9 +179,9 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveTunnelFromConfUri(name: String, uri: Uri) : Result<Unit> {
|
private suspend fun saveTunnelFromConfUri(name: String, uri: Uri): Result<Unit> {
|
||||||
val stream = getInputStreamFromUri(uri)
|
val stream = getInputStreamFromUri(uri)
|
||||||
return if(stream != null) {
|
return if (stream != null) {
|
||||||
saveTunnelConfigFromStream(stream, name)
|
saveTunnelConfigFromStream(stream, name)
|
||||||
Result.Success(Unit)
|
Result.Success(Unit)
|
||||||
} else {
|
} else {
|
||||||
|
@ -193,11 +194,13 @@ constructor(
|
||||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pauseAutoTunneling() = viewModelScope.launch {
|
fun pauseAutoTunneling() =
|
||||||
|
viewModelScope.launch {
|
||||||
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
|
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resumeAutoTunneling() = viewModelScope.launch {
|
fun resumeAutoTunneling() =
|
||||||
|
viewModelScope.launch {
|
||||||
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
|
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,6 +236,7 @@ constructor(
|
||||||
private fun isValidUriContentScheme(uri: Uri): Boolean {
|
private fun isValidUriContentScheme(uri: Uri): Boolean {
|
||||||
return uri.scheme == Constants.URI_CONTENT_SCHEME
|
return uri.scheme == Constants.URI_CONTENT_SCHEME
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFileName(context: Context, uri: Uri): String {
|
private fun getFileName(context: Context, uri: Uri): String {
|
||||||
return getFileNameByCursor(context, uri) ?: NumberUtils.generateRandomTunnelName()
|
return getFileNameByCursor(context, uri) ?: NumberUtils.generateRandomTunnelName()
|
||||||
}
|
}
|
||||||
|
@ -252,9 +256,11 @@ constructor(
|
||||||
private fun saveSettings(settings: Settings) =
|
private fun saveSettings(settings: Settings) =
|
||||||
viewModelScope.launch(Dispatchers.IO) { settingsRepository.save(settings) }
|
viewModelScope.launch(Dispatchers.IO) { settingsRepository.save(settings) }
|
||||||
|
|
||||||
fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) = viewModelScope.launch {
|
fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) =
|
||||||
|
viewModelScope.launch {
|
||||||
if (selectedTunnel != null) {
|
if (selectedTunnel != null) {
|
||||||
saveSettings(uiState.value.settings.copy(defaultTunnel = selectedTunnel.toString())).join()
|
saveSettings(uiState.value.settings.copy(defaultTunnel = selectedTunnel.toString()))
|
||||||
|
.join()
|
||||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
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(),
|
||||||
|
@ -115,6 +123,16 @@ fun SettingsScreen(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val startForResult =
|
||||||
|
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
result: ActivityResult ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
val intent = result.data
|
||||||
|
// Handle the Intent
|
||||||
|
}
|
||||||
|
viewModel.setBatteryOptimizeDisableShown()
|
||||||
|
}
|
||||||
|
|
||||||
fun exportAllConfigs() {
|
fun exportAllConfigs() {
|
||||||
try {
|
try {
|
||||||
val files = uiState.tunnels.map { File(context.cacheDir, "${it.name}.conf") }
|
val files = uiState.tunnels.map { File(context.cacheDir, "${it.name}.conf") }
|
||||||
|
@ -129,10 +147,32 @@ fun SettingsScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isBatteryOptimizationsDisabled(): Boolean {
|
||||||
|
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() {
|
fun saveTrustedSSID() {
|
||||||
if (currentText.isNotEmpty()) {
|
if (currentText.isNotEmpty()) {
|
||||||
viewModel.onSaveTrustedSSID(currentText).let {
|
viewModel.onSaveTrustedSSID(currentText).let {
|
||||||
when(it) {
|
when (it) {
|
||||||
is Result.Success -> currentText = ""
|
is Result.Success -> currentText = ""
|
||||||
is Result.Error -> showSnackbarMessage(it.error.message)
|
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||||
}
|
}
|
||||||
|
@ -159,7 +199,10 @@ fun SettingsScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
if(WireGuardAutoTunnel.isRunningOnAndroidTv() && Build.VERSION.SDK_INT == Build.VERSION_CODES.Q){
|
if (
|
||||||
|
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
||||||
|
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q
|
||||||
|
) {
|
||||||
checkFineLocationGranted()
|
checkFineLocationGranted()
|
||||||
} else {
|
} else {
|
||||||
val backgroundLocationState =
|
val backgroundLocationState =
|
||||||
|
@ -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,28 +240,33 @@ 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(
|
Icon(
|
||||||
Icons.Rounded.LocationOff,
|
Icons.Rounded.LocationOff,
|
||||||
contentDescription = stringResource(id = R.string.map),
|
contentDescription = stringResource(id = R.string.map),
|
||||||
modifier = Modifier.padding(30.dp).size(128.dp))
|
modifier = Modifier.padding(30.dp).size(128.dp),
|
||||||
|
)
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.prominent_background_location_title),
|
stringResource(R.string.prominent_background_location_title),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.padding(30.dp),
|
modifier = Modifier.padding(30.dp),
|
||||||
fontSize = 20.sp)
|
fontSize = 20.sp,
|
||||||
|
)
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.prominent_background_location_message),
|
stringResource(R.string.prominent_background_location_message),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.padding(30.dp),
|
modifier = Modifier.padding(30.dp),
|
||||||
fontSize = 15.sp)
|
fontSize = 15.sp,
|
||||||
|
)
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
|
@ -226,7 +275,8 @@ fun SettingsScreen(
|
||||||
Modifier.fillMaxWidth().padding(30.dp)
|
Modifier.fillMaxWidth().padding(30.dp)
|
||||||
},
|
},
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly) {
|
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))
|
||||||
}
|
}
|
||||||
|
@ -235,14 +285,15 @@ fun SettingsScreen(
|
||||||
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
|
||||||
|
@ -255,19 +306,22 @@ fun SettingsScreen(
|
||||||
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(
|
Text(
|
||||||
stringResource(R.string.one_tunnel_required),
|
stringResource(R.string.one_tunnel_required),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.padding(15.dp),
|
modifier = Modifier.padding(15.dp),
|
||||||
fontStyle = FontStyle.Italic)
|
fontStyle = FontStyle.Italic,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (uiState.isLocationDisclosureShown && uiState.tunnels.isNotEmpty()) {
|
if (uiState.isLocationDisclosureShown && uiState.tunnels.isNotEmpty()) {
|
||||||
|
@ -276,9 +330,12 @@ fun SettingsScreen(
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxSize().verticalScroll(scrollState).clickable(
|
Modifier.fillMaxSize().verticalScroll(scrollState).clickable(
|
||||||
indication = null, interactionSource = interactionSource) {
|
indication = null,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
) {
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
}) {
|
},
|
||||||
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
shadowElevation = 2.dp,
|
shadowElevation = 2.dp,
|
||||||
|
@ -292,14 +349,17 @@ fun SettingsScreen(
|
||||||
} else {
|
} else {
|
||||||
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 60.dp)
|
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 60.dp)
|
||||||
})
|
})
|
||||||
.padding(bottom = 10.dp)) {
|
.padding(bottom = 10.dp),
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier.padding(15.dp)) {
|
modifier = Modifier.padding(15.dp),
|
||||||
|
) {
|
||||||
SectionTitle(
|
SectionTitle(
|
||||||
title = stringResource(id = R.string.auto_tunneling),
|
title = stringResource(id = R.string.auto_tunneling),
|
||||||
padding = screenPadding)
|
padding = screenPadding,
|
||||||
|
)
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(id = R.string.tunnel_on_wifi),
|
stringResource(id = R.string.tunnel_on_wifi),
|
||||||
enabled =
|
enabled =
|
||||||
|
@ -308,30 +368,41 @@ fun SettingsScreen(
|
||||||
checked = uiState.settings.isTunnelOnWifiEnabled,
|
checked = uiState.settings.isTunnelOnWifiEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = { viewModel.onToggleTunnelOnWifi() },
|
onCheckChanged = { viewModel.onToggleTunnelOnWifi() },
|
||||||
modifier = if(uiState.settings.isAutoTunnelEnabled) Modifier else Modifier.focusRequester(focusRequester).focusProperties { down = focusRequester2 })
|
modifier =
|
||||||
|
if (uiState.settings.isAutoTunnelEnabled) Modifier
|
||||||
|
else
|
||||||
|
Modifier.focusRequester(focusRequester).focusProperties {
|
||||||
|
down = focusRequester2
|
||||||
|
},
|
||||||
|
)
|
||||||
AnimatedVisibility(visible = uiState.settings.isTunnelOnWifiEnabled) {
|
AnimatedVisibility(visible = uiState.settings.isTunnelOnWifiEnabled) {
|
||||||
Column {
|
Column {
|
||||||
FlowRow(
|
FlowRow(
|
||||||
modifier = Modifier.padding(screenPadding).fillMaxWidth(),
|
modifier = Modifier.padding(screenPadding).fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(5.dp)) {
|
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||||
|
) {
|
||||||
uiState.settings.trustedNetworkSSIDs.forEach { ssid ->
|
uiState.settings.trustedNetworkSSIDs.forEach { ssid ->
|
||||||
ClickableIconButton(
|
ClickableIconButton(
|
||||||
onClick = { if(WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
onClick = {
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
viewModel.onDeleteTrustedSSID(ssid)
|
viewModel.onDeleteTrustedSSID(ssid)
|
||||||
focusRequester2.requestFocus()
|
focusRequester2.requestFocus()
|
||||||
}},
|
}
|
||||||
|
},
|
||||||
onIconClick = { viewModel.onDeleteTrustedSSID(ssid) },
|
onIconClick = { viewModel.onDeleteTrustedSSID(ssid) },
|
||||||
text = ssid,
|
text = ssid,
|
||||||
icon = Icons.Filled.Close,
|
icon = Icons.Filled.Close,
|
||||||
enabled =
|
enabled =
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
uiState.settings.isAlwaysOnVpnEnabled))
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (uiState.settings.trustedNetworkSSIDs.isEmpty()) {
|
if (uiState.settings.trustedNetworkSSIDs.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.none),
|
stringResource(R.string.none),
|
||||||
fontStyle = FontStyle.Italic,
|
fontStyle = FontStyle.Italic,
|
||||||
color = Color.Gray)
|
color = Color.Gray,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
|
@ -343,14 +414,17 @@ fun SettingsScreen(
|
||||||
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.padding(
|
Modifier.padding(
|
||||||
start = screenPadding, top = 5.dp, bottom = 10.dp)
|
start = screenPadding,
|
||||||
.focusRequester(focusRequester2)
|
top = 5.dp,
|
||||||
,
|
bottom = 10.dp,
|
||||||
|
)
|
||||||
|
.focusRequester(focusRequester2),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
keyboardOptions =
|
keyboardOptions =
|
||||||
KeyboardOptions(
|
KeyboardOptions(
|
||||||
capitalization = KeyboardCapitalization.None,
|
capitalization = KeyboardCapitalization.None,
|
||||||
imeAction = ImeAction.Done),
|
imeAction = ImeAction.Done,
|
||||||
|
),
|
||||||
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
|
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
if (currentText != "") {
|
if (currentText != "") {
|
||||||
|
@ -360,15 +434,23 @@ fun SettingsScreen(
|
||||||
contentDescription =
|
contentDescription =
|
||||||
if (currentText == "") {
|
if (currentText == "") {
|
||||||
stringResource(
|
stringResource(
|
||||||
id = R.string.trusted_ssid_empty_description)
|
id =
|
||||||
|
R.string
|
||||||
|
.trusted_ssid_empty_description,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
stringResource(
|
stringResource(
|
||||||
id = R.string.trusted_ssid_value_description)
|
id =
|
||||||
|
R.string
|
||||||
|
.trusted_ssid_value_description,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
tint = MaterialTheme.colorScheme.primary)
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
|
@ -378,7 +460,8 @@ fun SettingsScreen(
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
checked = uiState.settings.isTunnelOnMobileDataEnabled,
|
checked = uiState.settings.isTunnelOnMobileDataEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = { viewModel.onToggleTunnelOnMobileData() })
|
onCheckChanged = { viewModel.onToggleTunnelOnMobileData() },
|
||||||
|
)
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(id = R.string.tunnel_on_ethernet),
|
stringResource(id = R.string.tunnel_on_ethernet),
|
||||||
enabled =
|
enabled =
|
||||||
|
@ -386,7 +469,8 @@ fun SettingsScreen(
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
checked = uiState.settings.isTunnelOnEthernetEnabled,
|
checked = uiState.settings.isTunnelOnEthernetEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = { viewModel.onToggleTunnelOnEthernet() })
|
onCheckChanged = { viewModel.onToggleTunnelOnEthernet() },
|
||||||
|
)
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(R.string.battery_saver),
|
stringResource(R.string.battery_saver),
|
||||||
enabled =
|
enabled =
|
||||||
|
@ -394,31 +478,47 @@ fun SettingsScreen(
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
checked = uiState.settings.isBatterySaverEnabled,
|
checked = uiState.settings.isBatterySaverEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = { viewModel.onToggleBatterySaver() })
|
onCheckChanged = { viewModel.onToggleBatterySaver() },
|
||||||
|
)
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = (if(!uiState.settings.isAutoTunnelEnabled) Modifier else Modifier.focusRequester(focusRequester))
|
modifier =
|
||||||
.fillMaxSize().padding(top = 5.dp),
|
(if (!uiState.settings.isAutoTunnelEnabled) Modifier
|
||||||
horizontalArrangement = Arrangement.Center) {
|
else
|
||||||
|
Modifier.focusRequester(
|
||||||
|
focusRequester,
|
||||||
|
))
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = 5.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
TextButton(
|
TextButton(
|
||||||
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
|
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
|
||||||
onClick = {
|
onClick = {
|
||||||
if (uiState.settings.isTunnelOnWifiEnabled && !uiState.settings.isAutoTunnelEnabled) {
|
if (
|
||||||
when(false) {
|
uiState.settings.isTunnelOnWifiEnabled &&
|
||||||
|
!uiState.settings.isAutoTunnelEnabled
|
||||||
|
) {
|
||||||
|
when (false) {
|
||||||
isBackgroundLocationGranted ->
|
isBackgroundLocationGranted ->
|
||||||
showSnackbarMessage(Event.Error.BackgroundLocationRequired.message)
|
showSnackbarMessage(
|
||||||
|
Event.Error.BackgroundLocationRequired.message
|
||||||
|
)
|
||||||
fineLocationState.status.isGranted ->
|
fineLocationState.status.isGranted ->
|
||||||
showSnackbarMessage(Event.Error.PreciseLocationRequired.message)
|
showSnackbarMessage(
|
||||||
|
Event.Error.PreciseLocationRequired.message
|
||||||
|
)
|
||||||
viewModel.isLocationEnabled(context) ->
|
viewModel.isLocationEnabled(context) ->
|
||||||
showLocationServicesAlertDialog = true
|
showLocationServicesAlertDialog = true
|
||||||
else -> {
|
else -> {
|
||||||
viewModel.toggleAutoTunnel()
|
handleAutoTunnelToggle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
viewModel.toggleAutoTunnel()
|
handleAutoTunnelToggle()
|
||||||
}
|
}
|
||||||
}) {
|
},
|
||||||
|
) {
|
||||||
val autoTunnelButtonText =
|
val autoTunnelButtonText =
|
||||||
if (uiState.settings.isAutoTunnelEnabled) {
|
if (uiState.settings.isAutoTunnelEnabled) {
|
||||||
stringResource(R.string.disable_auto_tunnel)
|
stringResource(R.string.disable_auto_tunnel)
|
||||||
|
@ -436,13 +536,17 @@ fun SettingsScreen(
|
||||||
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 = Modifier.fillMaxWidth(fillMaxWidth).padding(vertical = 10.dp),
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier.padding(15.dp)) {
|
modifier = Modifier.padding(15.dp),
|
||||||
|
) {
|
||||||
SectionTitle(
|
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,12 +555,15 @@ 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 {
|
||||||
|
when (it) {
|
||||||
is Result.Error -> showSnackbarMessage(it.error.message)
|
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||||
is Result.Success -> {}
|
is Result.Success -> {}
|
||||||
}
|
}
|
||||||
} })
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -469,31 +576,40 @@ fun SettingsScreen(
|
||||||
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(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier.padding(15.dp)) {
|
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(
|
TextButton(
|
||||||
enabled = !didExportFiles, onClick = { showAuthPrompt = true }) {
|
enabled = !didExportFiles,
|
||||||
|
onClick = { showAuthPrompt = true },
|
||||||
|
) {
|
||||||
Text(stringResource(R.string.export_configs))
|
Text(stringResource(R.string.export_configs))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 =
|
||||||
|
combine(
|
||||||
settingsRepository.getSettingsFlow(),
|
settingsRepository.getSettingsFlow(),
|
||||||
tunnelConfigRepository.getTunnelConfigsFlow(),
|
tunnelConfigRepository.getTunnelConfigsFlow(),
|
||||||
vpnService.vpnState,
|
vpnService.vpnState,
|
||||||
dataStoreManager.locationDisclosureFlow,
|
dataStoreManager.preferencesFlow,
|
||||||
){ settings, tunnels, tunnelState, locationDisclosure ->
|
) { settings, tunnels, tunnelState, preferences ->
|
||||||
SettingsUiState(settings, tunnels, tunnelState, locationDisclosure
|
SettingsUiState(
|
||||||
?: false, false)
|
settings,
|
||||||
}.stateIn(viewModelScope,
|
tunnels,
|
||||||
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), SettingsUiState())
|
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,29 +69,40 @@ 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(
|
saveSettings(
|
||||||
trustedNetworkSSIDs = (uiState.value.settings.trustedNetworkSSIDs - ssid).toMutableList()
|
uiState.value.settings.copy(
|
||||||
))
|
trustedNetworkSSIDs =
|
||||||
|
(uiState.value.settings.trustedNetworkSSIDs - ssid).toMutableList(),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getDefaultTunnelOrFirst() : String {
|
private suspend fun getDefaultTunnelOrFirst(): String {
|
||||||
return uiState.value.settings.defaultTunnel ?: tunnelConfigRepository.getAll().first().toString()
|
return uiState.value.settings.defaultTunnel
|
||||||
|
?: tunnelConfigRepository.getAll().first().toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleAutoTunnel() = viewModelScope.launch {
|
fun toggleAutoTunnel() =
|
||||||
|
viewModelScope.launch {
|
||||||
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
|
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
|
||||||
var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused
|
var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused
|
||||||
|
|
||||||
|
@ -94,28 +116,30 @@ constructor(
|
||||||
uiState.value.settings.copy(
|
uiState.value.settings.copy(
|
||||||
isAutoTunnelEnabled = !isAutoTunnelEnabled,
|
isAutoTunnelEnabled = !isAutoTunnelEnabled,
|
||||||
isAutoTunnelPaused = isAutoTunnelPaused,
|
isAutoTunnelPaused = isAutoTunnelPaused,
|
||||||
defaultTunnel = getDefaultTunnelOrFirst()
|
defaultTunnel = getDefaultTunnelOrFirst(),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onToggleAlwaysOnVPN() =
|
||||||
fun onToggleAlwaysOnVPN() = viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val updatedSettings = uiState.value.settings.copy(
|
val updatedSettings =
|
||||||
|
uiState.value.settings.copy(
|
||||||
isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled,
|
isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled,
|
||||||
defaultTunnel = getDefaultTunnelOrFirst()
|
defaultTunnel = getDefaultTunnelOrFirst(),
|
||||||
)
|
)
|
||||||
saveSettings(updatedSettings)
|
saveSettings(updatedSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveSettings(settings: Settings) = viewModelScope.launch {
|
private fun saveSettings(settings: Settings) =
|
||||||
settingsRepository.save(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()
|
||||||
|
|
|
@ -72,7 +72,7 @@ fun SupportScreen(
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,13 +80,17 @@ fun SupportScreen(
|
||||||
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,
|
||||||
|
createChooser(intent, context.getString(R.string.email_chooser)),
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
showSnackbarMessage(Event.Error.Exception(e).message)
|
showSnackbarMessage(Event.Error.Exception(e).message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -103,7 +107,8 @@ fun SupportScreen(
|
||||||
Modifier.fillMaxSize()
|
Modifier.fillMaxSize()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.focusable()
|
.focusable()
|
||||||
.padding(padding)) {
|
.padding(padding),
|
||||||
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
shadowElevation = 2.dp,
|
shadowElevation = 2.dp,
|
||||||
|
@ -117,32 +122,38 @@ fun SupportScreen(
|
||||||
} 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)) {
|
Column(modifier = Modifier.padding(20.dp)) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.thank_you),
|
stringResource(R.string.thank_you),
|
||||||
textAlign = TextAlign.Start,
|
textAlign = TextAlign.Start,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
modifier = Modifier.padding(bottom = 20.dp),
|
modifier = Modifier.padding(bottom = 20.dp),
|
||||||
fontSize = 16.sp)
|
fontSize = 16.sp,
|
||||||
|
)
|
||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.support_help_text),
|
stringResource(id = R.string.support_help_text),
|
||||||
textAlign = TextAlign.Start,
|
textAlign = TextAlign.Start,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
modifier = Modifier.padding(bottom = 20.dp))
|
modifier = Modifier.padding(bottom = 20.dp),
|
||||||
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { openWebPage(context.resources.getString(R.string.docs_url)) },
|
onClick = { openWebPage(context.resources.getString(R.string.docs_url)) },
|
||||||
modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester)) {
|
modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester),
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxWidth()) {
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
Row {
|
Row {
|
||||||
Icon(Icons.Rounded.Book, stringResource(id = R.string.docs))
|
Icon(Icons.Rounded.Book, stringResource(id = R.string.docs))
|
||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.docs_description),
|
stringResource(id = R.string.docs_description),
|
||||||
textAlign = TextAlign.Justify,
|
textAlign = TextAlign.Justify,
|
||||||
modifier = Modifier.padding(start = 10.dp))
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||||
}
|
}
|
||||||
|
@ -150,20 +161,24 @@ fun SupportScreen(
|
||||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { openWebPage(context.resources.getString(R.string.discord_url)) },
|
onClick = { openWebPage(context.resources.getString(R.string.discord_url)) },
|
||||||
modifier = Modifier.padding(vertical = 5.dp)) {
|
modifier = Modifier.padding(vertical = 5.dp),
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxWidth()) {
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
Row {
|
Row {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = ImageVector.vectorResource(R.drawable.discord),
|
imageVector = ImageVector.vectorResource(R.drawable.discord),
|
||||||
stringResource(id = R.string.discord),
|
stringResource(id = R.string.discord),
|
||||||
Modifier.size(25.dp))
|
Modifier.size(25.dp),
|
||||||
|
)
|
||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.discord_description),
|
stringResource(id = R.string.discord_description),
|
||||||
textAlign = TextAlign.Justify,
|
textAlign = TextAlign.Justify,
|
||||||
modifier = Modifier.padding(start = 10.dp))
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||||
}
|
}
|
||||||
|
@ -171,37 +186,45 @@ fun SupportScreen(
|
||||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { openWebPage(context.resources.getString(R.string.github_url)) },
|
onClick = { openWebPage(context.resources.getString(R.string.github_url)) },
|
||||||
modifier = Modifier.padding(vertical = 5.dp)) {
|
modifier = Modifier.padding(vertical = 5.dp),
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxWidth()) {
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
Row {
|
Row {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = ImageVector.vectorResource(R.drawable.github),
|
imageVector = ImageVector.vectorResource(R.drawable.github),
|
||||||
stringResource(id = R.string.github),
|
stringResource(id = R.string.github),
|
||||||
Modifier.size(25.dp))
|
Modifier.size(25.dp),
|
||||||
|
)
|
||||||
Text(
|
Text(
|
||||||
"Open an issue",
|
"Open an issue",
|
||||||
textAlign = TextAlign.Justify,
|
textAlign = TextAlign.Justify,
|
||||||
modifier = Modifier.padding(start = 10.dp))
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { launchEmail() }, modifier = Modifier.padding(vertical = 5.dp)) {
|
onClick = { launchEmail() },
|
||||||
|
modifier = Modifier.padding(vertical = 5.dp),
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxWidth()) {
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
Row {
|
Row {
|
||||||
Icon(Icons.Rounded.Mail, stringResource(id = R.string.email))
|
Icon(Icons.Rounded.Mail, stringResource(id = R.string.email))
|
||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.email_description),
|
stringResource(id = R.string.email_description),
|
||||||
textAlign = TextAlign.Justify,
|
textAlign = TextAlign.Justify,
|
||||||
modifier = Modifier.padding(start = 10.dp))
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||||
}
|
}
|
||||||
|
@ -216,13 +239,15 @@ fun SupportScreen(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.clickable {
|
Modifier.clickable {
|
||||||
openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
||||||
})
|
},
|
||||||
|
)
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(25.dp),
|
horizontalArrangement = Arrangement.spacedBy(25.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.padding(25.dp)) {
|
modifier = Modifier.padding(25.dp),
|
||||||
|
) {
|
||||||
Text("Version: ${BuildConfig.VERSION_NAME}", modifier = Modifier.focusable())
|
Text("Version: ${BuildConfig.VERSION_NAME}", modifier = Modifier.focusable())
|
||||||
Text("Mode: ${if (uiState.settings.isKernelEnabled) "Kernel" else "Userspace" }")
|
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()
|
||||||
|
.map { SupportUiState(it, false) }
|
||||||
|
.stateIn(
|
||||||
viewModelScope,
|
viewModelScope,
|
||||||
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
||||||
SupportUiState()
|
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 {}
|
||||||
|
|
|
@ -15,8 +15,8 @@ val Typography =
|
||||||
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,77 +12,98 @@ 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,21 +36,19 @@ 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(
|
||||||
) {
|
|
||||||
val zipOutputStream = createDownloadsFileOutputStream(
|
|
||||||
context,
|
context,
|
||||||
"wg-export_${Instant.now().epochSecond}.zip",
|
"wg-export_${Instant.now().epochSecond}.zip",
|
||||||
ZIP_FILE_MIME_TYPE
|
ZIP_FILE_MIME_TYPE,
|
||||||
)
|
)
|
||||||
ZipOutputStream(zipOutputStream).use { zos ->
|
ZipOutputStream(zipOutputStream).use { zos ->
|
||||||
files.forEach { file ->
|
files.forEach { file ->
|
||||||
|
|
|
@ -2,12 +2,12 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
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>
|
||||||
|
|
|
@ -4,9 +4,9 @@
|
||||||
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:strokeWidth="1"
|
|
||||||
android:fillColor="#000000"
|
android:fillColor="#000000"
|
||||||
android:fillType="evenOdd"
|
android:fillType="evenOdd"
|
||||||
android:strokeColor="#00000000"/>
|
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:strokeWidth="1"
|
||||||
|
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"
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
|
@ -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