fix: tunnel disable frozen
Fixes a bug where after toggling a tunnel so many times it would eventually get stuck in the on position. This was also impacting auto-tunneling reliability. Fixes a bug where clicking the email button on the support page would not populate the "to" email field. Fixes a bug where you could not save a tunnel without having configured DNS. Added a dialog to prompt user if they are deleting a tunnel. Added battery optimization disable request when first launching auto-tunneling. Format to kotlinlang standards. Fix ci google play deploy. Closes #63
This commit is contained in:
parent
7ec294b789
commit
5a15776bb3
|
@ -0,0 +1,85 @@
|
|||
[{*.kt,*.kts}]
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
max_line_length = 100
|
||||
indent_size = 4
|
||||
ij_continuation_indent_size = 4
|
||||
ij_java_names_count_to_use_import_on_demand = 9999
|
||||
ij_kotlin_align_in_columns_case_branch = false
|
||||
ij_kotlin_align_multiline_binary_operation = false
|
||||
ij_kotlin_align_multiline_extends_list = false
|
||||
ij_kotlin_align_multiline_method_parentheses = false
|
||||
ij_kotlin_align_multiline_parameters = true
|
||||
ij_kotlin_align_multiline_parameters_in_calls = false
|
||||
ij_kotlin_allow_trailing_comma = true
|
||||
ij_kotlin_allow_trailing_comma_on_call_site = true
|
||||
ij_kotlin_assignment_wrap = normal
|
||||
ij_kotlin_blank_lines_after_class_header = 0
|
||||
ij_kotlin_blank_lines_around_block_when_branches = 0
|
||||
ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1
|
||||
ij_kotlin_block_comment_at_first_column = true
|
||||
ij_kotlin_call_parameters_new_line_after_left_paren = true
|
||||
ij_kotlin_call_parameters_right_paren_on_new_line = false
|
||||
ij_kotlin_call_parameters_wrap = on_every_item
|
||||
ij_kotlin_catch_on_new_line = false
|
||||
ij_kotlin_class_annotation_wrap = split_into_lines
|
||||
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
|
||||
ij_kotlin_continuation_indent_for_chained_calls = true
|
||||
ij_kotlin_continuation_indent_for_expression_bodies = true
|
||||
ij_kotlin_continuation_indent_in_argument_lists = true
|
||||
ij_kotlin_continuation_indent_in_elvis = false
|
||||
ij_kotlin_continuation_indent_in_if_conditions = false
|
||||
ij_kotlin_continuation_indent_in_parameter_lists = false
|
||||
ij_kotlin_continuation_indent_in_supertype_lists = false
|
||||
ij_kotlin_else_on_new_line = false
|
||||
ij_kotlin_enum_constants_wrap = off
|
||||
ij_kotlin_extends_list_wrap = normal
|
||||
ij_kotlin_field_annotation_wrap = split_into_lines
|
||||
ij_kotlin_finally_on_new_line = false
|
||||
ij_kotlin_if_rparen_on_new_line = false
|
||||
ij_kotlin_import_nested_classes = false
|
||||
ij_kotlin_insert_whitespaces_in_simple_one_line_method = true
|
||||
ij_kotlin_keep_blank_lines_before_right_brace = 2
|
||||
ij_kotlin_keep_blank_lines_in_code = 2
|
||||
ij_kotlin_keep_blank_lines_in_declarations = 2
|
||||
ij_kotlin_keep_first_column_comment = true
|
||||
ij_kotlin_keep_indents_on_empty_lines = false
|
||||
ij_kotlin_keep_line_breaks = true
|
||||
ij_kotlin_lbrace_on_next_line = false
|
||||
ij_kotlin_line_comment_add_space = false
|
||||
ij_kotlin_line_comment_at_first_column = true
|
||||
ij_kotlin_method_annotation_wrap = split_into_lines
|
||||
ij_kotlin_method_call_chain_wrap = normal
|
||||
ij_kotlin_method_parameters_new_line_after_left_paren = true
|
||||
ij_kotlin_method_parameters_right_paren_on_new_line = true
|
||||
ij_kotlin_method_parameters_wrap = on_every_item
|
||||
ij_kotlin_name_count_to_use_star_import = 9999
|
||||
ij_kotlin_name_count_to_use_star_import_for_members = 9999
|
||||
ij_kotlin_parameter_annotation_wrap = off
|
||||
ij_kotlin_space_after_comma = true
|
||||
ij_kotlin_space_after_extend_colon = true
|
||||
ij_kotlin_space_after_type_colon = true
|
||||
ij_kotlin_space_before_catch_parentheses = true
|
||||
ij_kotlin_space_before_comma = false
|
||||
ij_kotlin_space_before_extend_colon = true
|
||||
ij_kotlin_space_before_for_parentheses = true
|
||||
ij_kotlin_space_before_if_parentheses = true
|
||||
ij_kotlin_space_before_lambda_arrow = true
|
||||
ij_kotlin_space_before_type_colon = false
|
||||
ij_kotlin_space_before_when_parentheses = true
|
||||
ij_kotlin_space_before_while_parentheses = true
|
||||
ij_kotlin_spaces_around_additive_operators = true
|
||||
ij_kotlin_spaces_around_assignment_operators = true
|
||||
ij_kotlin_spaces_around_equality_operators = true
|
||||
ij_kotlin_spaces_around_function_type_arrow = true
|
||||
ij_kotlin_spaces_around_logical_operators = true
|
||||
ij_kotlin_spaces_around_multiplicative_operators = true
|
||||
ij_kotlin_spaces_around_range = false
|
||||
ij_kotlin_spaces_around_relational_operators = true
|
||||
ij_kotlin_spaces_around_unary_operator = false
|
||||
ij_kotlin_spaces_around_when_arrow = true
|
||||
ij_kotlin_variable_annotation_wrap = off
|
||||
ij_kotlin_while_on_new_line = false
|
||||
ij_kotlin_wrap_elvis_expressions = 1
|
||||
ij_kotlin_wrap_expression_body_functions = 1
|
||||
ij_kotlin_wrap_first_method_in_call_chain = false
|
|
@ -11,12 +11,14 @@ assignees: zaneschepke
|
|||
A clear and concise description of what the bug is.
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- Android Version: [e.g. iOS8.1]
|
||||
- App Version [e.g. 22]
|
||||
|
||||
- Device: [e.g. Pixel 4a]
|
||||
- Android Version: [e.g. Android 13]
|
||||
- App Version [e.g. 3.3.3]
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
|
|
|
@ -9,7 +9,7 @@ on:
|
|||
jobs:
|
||||
build:
|
||||
name: Build Signed APK
|
||||
# change to macos because of hilt issues on ubuntu in gradle 8.3
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
|
@ -70,7 +70,7 @@ jobs:
|
|||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
# 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 }}
|
||||
name: Release ${{ github.ref_name }}
|
||||
draft: false
|
||||
|
|
|
@ -28,7 +28,10 @@ WG Tunnel
|
|||
|
||||
<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>
|
||||
|
||||
|
@ -47,7 +50,8 @@ This is an alternative Android Application for [WireGuard](https://www.wireguard
|
|||
|
||||
## 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
|
||||
|
||||
|
@ -63,7 +67,6 @@ The original inspiration for this app came from the inconvenience of having to m
|
|||
* Automatic service restart after reboot
|
||||
* Battery preservation measures
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
|
|
|
@ -19,9 +19,7 @@ android {
|
|||
versionCode = Constants.VERSION_CODE
|
||||
versionName = Constants.VERSION_NAME
|
||||
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
ksp { arg("room.schemaLocation", "$projectDir/schemas") }
|
||||
|
||||
sourceSets {
|
||||
getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room
|
||||
|
@ -30,9 +28,7 @@ android {
|
|||
resourceConfigurations.addAll(listOf("en"))
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
}
|
||||
vectorDrawables { useSupportLibrary = true }
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
|
@ -47,24 +43,33 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
// try to get secrets from env first for pipeline build, then properties file for local build
|
||||
storeFile = file(
|
||||
System.getenv().getOrDefault(
|
||||
// try to get secrets from env first for pipeline build, then properties file for local
|
||||
// build
|
||||
storeFile =
|
||||
file(
|
||||
System.getenv()
|
||||
.getOrDefault(
|
||||
Constants.KEY_STORE_PATH_VAR,
|
||||
properties.getProperty(Constants.KEY_STORE_PATH_VAR)
|
||||
properties.getProperty(Constants.KEY_STORE_PATH_VAR),
|
||||
),
|
||||
)
|
||||
)
|
||||
storePassword = System.getenv().getOrDefault(
|
||||
storePassword =
|
||||
System.getenv()
|
||||
.getOrDefault(
|
||||
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,
|
||||
properties.getProperty(Constants.KEY_ALIAS_VAR)
|
||||
properties.getProperty(Constants.KEY_ALIAS_VAR),
|
||||
)
|
||||
keyPassword = System.getenv().getOrDefault(
|
||||
keyPassword =
|
||||
System.getenv()
|
||||
.getOrDefault(
|
||||
Constants.KEY_PASS_VAR,
|
||||
properties.getProperty(Constants.KEY_PASS_VAR)
|
||||
properties.getProperty(Constants.KEY_PASS_VAR),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -72,7 +77,7 @@ android {
|
|||
buildTypes {
|
||||
// don't strip
|
||||
packaging.jniLibs.keepDebugSymbols.addAll(
|
||||
listOf("libwg-go.so", "libwg-quick.so", "libwg.so")
|
||||
listOf("libwg-go.so", "libwg-quick.so", "libwg.so"),
|
||||
)
|
||||
|
||||
applicationVariants.all {
|
||||
|
@ -91,13 +96,11 @@ android {
|
|||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
signingConfig = signingConfigs.getByName(Constants.RELEASE)
|
||||
}
|
||||
debug {
|
||||
isDebuggable = true
|
||||
}
|
||||
debug { isDebuggable = true }
|
||||
}
|
||||
flavorDimensions.add(Constants.TYPE)
|
||||
productFlavors {
|
||||
|
@ -118,24 +121,17 @@ android {
|
|||
targetCompatibility = JavaVersion.VERSION_17
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = Constants.JVM_TARGET
|
||||
}
|
||||
kotlinOptions { jvmTarget = Constants.JVM_TARGET }
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
|
||||
}
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
}
|
||||
composeOptions { kotlinCompilerExtensionVersion = Constants.COMPOSE_COMPILER_EXTENSION_VERSION }
|
||||
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
|
||||
}
|
||||
|
||||
val generalImplementation by configurations
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
|
|
|
@ -14,9 +14,10 @@ class MigrationTest {
|
|||
private val dbName = "migration-test"
|
||||
|
||||
@get:Rule
|
||||
val helper: MigrationTestHelper = MigrationTestHelper(
|
||||
val helper: MigrationTestHelper =
|
||||
MigrationTestHelper(
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
AppDatabase::class.java
|
||||
AppDatabase::class.java,
|
||||
)
|
||||
|
||||
@Test
|
||||
|
@ -50,11 +51,10 @@ class MigrationTest {
|
|||
"'false'," +
|
||||
"'false'," +
|
||||
"'false'," +
|
||||
"'false')"
|
||||
"'false')",
|
||||
)
|
||||
execSQL(
|
||||
"INSERT INTO TunnelConfig (name, wg_quick)" +
|
||||
" VALUES ('hello', 'hello')"
|
||||
"INSERT INTO TunnelConfig (name, wg_quick)" + " VALUES ('hello', 'hello')",
|
||||
)
|
||||
// Prepare for the next version.
|
||||
close()
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<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" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"
|
||||
<uses-permission
|
||||
android:name="android.permission.ACCESS_WIFI_STATE"
|
||||
android:maxSdkVersion="30"
|
||||
tools:ignore="LeanbackUsesWifi" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
@ -25,12 +28,15 @@
|
|||
<!--foreground service permissions-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<!--start service on boot permission-->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<!--android tv support-->
|
||||
<uses-feature android:name="android.software.leanback"
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
<uses-feature android:name="android.hardware.touchscreen"
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.location.gps"
|
||||
|
@ -38,19 +44,20 @@
|
|||
<uses-feature
|
||||
android:name="android.hardware.screen.portrait"
|
||||
android:required="false" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent>
|
||||
</queries>
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:name=".WireGuardAutoTunnel"
|
||||
android:allowBackup="true"
|
||||
android:banner="@mipmap/ic_banner"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:banner="@mipmap/ic_banner"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
|
@ -62,11 +69,14 @@
|
|||
android:theme="@style/Theme.WireguardAutoTunnel">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.app.shortcuts"
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
<activity
|
||||
|
@ -76,65 +86,74 @@
|
|||
android:theme="@style/zxing_CaptureTheme"
|
||||
android:windowSoftInputMode="stateAlwaysHidden" />
|
||||
<activity
|
||||
android:finishOnTaskLaunch="true"
|
||||
android:name=".service.shortcut.ShortcutsActivity"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
android:name=".service.shortcut.ShortcutsActivity"/>
|
||||
android:finishOnTaskLaunch="true"
|
||||
android:theme="@android:style/Theme.NoDisplay" />
|
||||
|
||||
<service
|
||||
android:name=".service.foreground.ForegroundService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="systemExempted|specialUse"
|
||||
tools:node="merge"
|
||||
android:exported="false">
|
||||
</service>
|
||||
tools:node="merge" />
|
||||
<service
|
||||
android:exported="true"
|
||||
android:name=".service.tile.TunnelControlTile"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/shield"
|
||||
android:label="WG Tunnel"
|
||||
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" />
|
||||
<meta-data android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||
android:value="true" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".service.foreground.WireGuardTunnelService"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:enabled="true"
|
||||
android:persistent="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="systemExempted|specialUse"
|
||||
tools:node="merge"
|
||||
android:exported="false">
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:persistent="true"
|
||||
tools:node="merge">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
</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" />
|
||||
</service>
|
||||
<service
|
||||
android:name=".service.foreground.WireGuardConnectivityWatcherService"
|
||||
android:enabled="true"
|
||||
android:stopWithTask="false"
|
||||
android:persistent="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="systemExempted|specialUse"
|
||||
tools:node="merge"
|
||||
android:exported="false">
|
||||
</service>
|
||||
<receiver android:enabled="true" android:name=".receiver.BootReceiver"
|
||||
android:persistent="true"
|
||||
android:stopWithTask="false"
|
||||
tools:node="merge" />
|
||||
|
||||
<receiver
|
||||
android:name=".receiver.BootReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:exported="false" android:name=".receiver.NotificationActionReceiver"/>
|
||||
<receiver
|
||||
android:name=".receiver.NotificationActionReceiver"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
</manifest>
|
|
@ -4,61 +4,31 @@ import android.app.Application
|
|||
import android.content.ComponentName
|
||||
import android.content.pm.PackageManager
|
||||
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 dagger.hilt.android.HiltAndroidApp
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidApp
|
||||
class WireGuardAutoTunnel : Application() {
|
||||
@Inject
|
||||
lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var dataStoreManager: DataStoreManager
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
instance = this
|
||||
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 {
|
||||
lateinit var instance: WireGuardAutoTunnel private set
|
||||
lateinit var instance: WireGuardAutoTunnel
|
||||
private set
|
||||
|
||||
fun isRunningOnAndroidTv(): Boolean {
|
||||
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||
}
|
||||
|
||||
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(
|
||||
entities = [Settings::class, TunnelConfig::class],
|
||||
version = 5,
|
||||
autoMigrations = [
|
||||
AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3), AutoMigration(
|
||||
autoMigrations =
|
||||
[
|
||||
AutoMigration(from = 1, to = 2),
|
||||
AutoMigration(from = 2, to = 3),
|
||||
AutoMigration(
|
||||
from = 3,
|
||||
to = 4
|
||||
),AutoMigration(
|
||||
to = 4,
|
||||
),
|
||||
AutoMigration(
|
||||
from = 4,
|
||||
to = 5
|
||||
)
|
||||
to = 5,
|
||||
),
|
||||
],
|
||||
exportSchema = true
|
||||
exportSchema = true,
|
||||
)
|
||||
@TypeConverters(DatabaseListConverters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
|
|
|
@ -10,27 +10,19 @@ import kotlinx.coroutines.flow.Flow
|
|||
|
||||
@Dao
|
||||
interface SettingsDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun save(t: Settings)
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: Settings)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun saveAll(t: List<Settings>)
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<Settings>)
|
||||
|
||||
@Query("SELECT * FROM settings WHERE id=:id")
|
||||
suspend fun getById(id: Long): Settings?
|
||||
@Query("SELECT * FROM settings WHERE id=:id") suspend fun getById(id: Long): Settings?
|
||||
|
||||
@Query("SELECT * FROM settings")
|
||||
suspend fun getAll(): List<Settings>
|
||||
@Query("SELECT * FROM settings") suspend fun getAll(): List<Settings>
|
||||
|
||||
@Query("SELECT * FROM settings LIMIT 1")
|
||||
fun getSettingsFlow(): Flow<Settings>
|
||||
@Query("SELECT * FROM settings LIMIT 1") fun getSettingsFlow(): Flow<Settings>
|
||||
|
||||
@Query("SELECT * FROM settings")
|
||||
fun getAllFlow(): Flow<MutableList<Settings>>
|
||||
@Query("SELECT * FROM settings") fun getAllFlow(): Flow<MutableList<Settings>>
|
||||
|
||||
@Delete
|
||||
suspend fun delete(t: Settings)
|
||||
@Delete suspend fun delete(t: Settings)
|
||||
|
||||
@Query("SELECT COUNT('id') FROM settings")
|
||||
suspend fun count(): Long
|
||||
@Query("SELECT COUNT('id') FROM settings") suspend fun count(): Long
|
||||
}
|
||||
|
|
|
@ -10,24 +10,17 @@ import kotlinx.coroutines.flow.Flow
|
|||
|
||||
@Dao
|
||||
interface TunnelConfigDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun save(t: TunnelConfig)
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: TunnelConfig)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun saveAll(t: List<TunnelConfig>)
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<TunnelConfig>)
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig WHERE id=:id")
|
||||
suspend fun getById(id: Long): TunnelConfig?
|
||||
@Query("SELECT * FROM TunnelConfig WHERE id=:id") suspend fun getById(id: Long): TunnelConfig?
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig")
|
||||
suspend fun getAll(): List<TunnelConfig>
|
||||
@Query("SELECT * FROM TunnelConfig") suspend fun getAll(): List<TunnelConfig>
|
||||
|
||||
@Delete
|
||||
suspend fun delete(t: TunnelConfig)
|
||||
@Delete suspend fun delete(t: TunnelConfig)
|
||||
|
||||
@Query("SELECT COUNT('id') FROM TunnelConfig")
|
||||
suspend fun count(): Long
|
||||
@Query("SELECT COUNT('id') FROM TunnelConfig") suspend fun count(): Long
|
||||
|
||||
@Query("SELECT * FROM tunnelconfig")
|
||||
fun getAllFlow(): Flow<MutableList<TunnelConfig>>
|
||||
@Query("SELECT * FROM tunnelconfig") fun getAllFlow(): Flow<MutableList<TunnelConfig>>
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
package com.zaneschepke.wireguardautotunnel.data.datastore
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
|
@ -11,12 +12,14 @@ import kotlinx.coroutines.flow.map
|
|||
class DataStoreManager(private val context: Context) {
|
||||
companion object {
|
||||
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
|
||||
val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
|
||||
}
|
||||
|
||||
// preferences
|
||||
private val preferencesKey = "preferences"
|
||||
private val Context.dataStore by preferencesDataStore(
|
||||
name = preferencesKey
|
||||
private val Context.dataStore by
|
||||
preferencesDataStore(
|
||||
name = preferencesKey,
|
||||
)
|
||||
|
||||
suspend fun init() {
|
||||
|
@ -24,16 +27,12 @@ class DataStoreManager(private val context: Context) {
|
|||
}
|
||||
|
||||
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) =
|
||||
context.dataStore.edit {
|
||||
it[key] = value
|
||||
}
|
||||
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map {
|
||||
it[key]
|
||||
}
|
||||
context.dataStore.edit { it[key] = value }
|
||||
|
||||
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 {
|
||||
it[LOCATION_DISCLOSURE_SHOWN]
|
||||
}
|
||||
suspend fun <T> getFromStore(key: Preferences.Key<T>) =
|
||||
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(
|
||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||
@ColumnInfo(name = "is_tunnel_enabled") var isAutoTunnelEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled") var isTunnelOnMobileDataEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "trusted_network_ssids") var trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
|
||||
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled")
|
||||
var isTunnelOnMobileDataEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "trusted_network_ssids")
|
||||
var trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
|
||||
@ColumnInfo(name = "default_tunnel") var defaultTunnel: String? = null,
|
||||
@ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled") var isTunnelOnEthernetEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled")
|
||||
var isTunnelOnEthernetEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_shortcuts_enabled",
|
||||
defaultValue = "false"
|
||||
) var isShortcutsEnabled: Boolean = false,
|
||||
defaultValue = "false",
|
||||
)
|
||||
var isShortcutsEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_battery_saver_enabled",
|
||||
defaultValue = "false"
|
||||
) var isBatterySaverEnabled: Boolean = false,
|
||||
defaultValue = "false",
|
||||
)
|
||||
var isBatterySaverEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_tunnel_on_wifi_enabled",
|
||||
defaultValue = "false"
|
||||
) var isTunnelOnWifiEnabled: Boolean = false,
|
||||
defaultValue = "false",
|
||||
)
|
||||
var isTunnelOnWifiEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_kernel_enabled",
|
||||
defaultValue = "false"
|
||||
) var isKernelEnabled: Boolean = false,
|
||||
defaultValue = "false",
|
||||
)
|
||||
var isKernelEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_restore_on_boot_enabled",
|
||||
defaultValue = "false"
|
||||
) var isRestoreOnBootEnabled: Boolean = false,
|
||||
defaultValue = "false",
|
||||
)
|
||||
var isRestoreOnBootEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_multi_tunnel_enabled",
|
||||
defaultValue = "false"
|
||||
) var isMultiTunnelEnabled: Boolean = false,
|
||||
defaultValue = "false",
|
||||
)
|
||||
var isMultiTunnelEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_auto_tunnel_paused",
|
||||
defaultValue = "false"
|
||||
) var isAutoTunnelPaused: Boolean = false,
|
||||
defaultValue = "false",
|
||||
)
|
||||
var isAutoTunnelPaused: Boolean = false,
|
||||
) {
|
||||
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean {
|
||||
return if (defaultTunnel != null) {
|
||||
|
|
|
@ -5,8 +5,10 @@ import kotlinx.coroutines.flow.Flow
|
|||
|
||||
interface SettingsRepository {
|
||||
suspend fun save(settings: Settings)
|
||||
|
||||
fun getSettingsFlow(): Flow<Settings>
|
||||
|
||||
suspend fun getSettings(): Settings
|
||||
|
||||
suspend fun getAll(): List<Settings>
|
||||
}
|
|
@ -7,8 +7,12 @@ import kotlinx.coroutines.flow.Flow
|
|||
interface TunnelConfigRepository {
|
||||
|
||||
fun getTunnelConfigsFlow(): Flow<TunnelConfigs>
|
||||
|
||||
suspend fun getAll(): TunnelConfigs
|
||||
|
||||
suspend fun save(tunnelConfig: TunnelConfig)
|
||||
|
||||
suspend fun delete(tunnelConfig: TunnelConfig)
|
||||
|
||||
suspend fun count(): Int
|
||||
}
|
|
@ -5,7 +5,8 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
|||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class TunnelConfigRepositoryImpl(private val tunnelConfigDao: TunnelConfigDao) : TunnelConfigRepository {
|
||||
class TunnelConfigRepositoryImpl(private val tunnelConfigDao: TunnelConfigDao) :
|
||||
TunnelConfigRepository {
|
||||
override fun getTunnelConfigsFlow(): Flow<TunnelConfigs> {
|
||||
return tunnelConfigDao.getAllFlow()
|
||||
}
|
||||
|
|
|
@ -16,13 +16,11 @@ import javax.inject.Singleton
|
|||
class DatabaseModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDatabase(
|
||||
@ApplicationContext context: Context
|
||||
): AppDatabase {
|
||||
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
|
||||
return Room.databaseBuilder(
|
||||
context,
|
||||
AppDatabase::class.java,
|
||||
context.getString(R.string.db_name)
|
||||
context.getString(R.string.db_name),
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
|
|
|
@ -2,6 +2,4 @@ package com.zaneschepke.wireguardautotunnel.module
|
|||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class Kernel
|
||||
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Kernel
|
||||
|
|
|
@ -17,7 +17,9 @@ import dagger.hilt.android.scopes.ServiceScoped
|
|||
abstract class ServiceModule {
|
||||
@Binds
|
||||
@ServiceScoped
|
||||
abstract fun provideNotificationService(wireGuardNotification: WireGuardNotification): NotificationService
|
||||
abstract fun provideNotificationService(
|
||||
wireGuardNotification: WireGuardNotification
|
||||
): NotificationService
|
||||
|
||||
@Binds
|
||||
@ServiceScoped
|
||||
|
@ -25,9 +27,13 @@ abstract class ServiceModule {
|
|||
|
||||
@Binds
|
||||
@ServiceScoped
|
||||
abstract fun provideMobileDataService(mobileDataService: MobileDataService): NetworkService<MobileDataService>
|
||||
abstract fun provideMobileDataService(
|
||||
mobileDataService: MobileDataService
|
||||
): NetworkService<MobileDataService>
|
||||
|
||||
@Binds
|
||||
@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 {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRootShell(
|
||||
@ApplicationContext context: Context
|
||||
): RootShell {
|
||||
fun provideRootShell(@ApplicationContext context: Context): RootShell {
|
||||
return RootShell(context)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Userspace
|
||||
fun provideUserspaceBackend(
|
||||
@ApplicationContext context: Context
|
||||
): Backend {
|
||||
fun provideUserspaceBackend(@ApplicationContext context: Context): Backend {
|
||||
return GoBackend(context)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Kernel
|
||||
fun provideKernelBackend(
|
||||
@ApplicationContext context: Context,
|
||||
rootShell: RootShell
|
||||
): Backend {
|
||||
fun provideKernelBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend {
|
||||
return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell))
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,4 @@ package com.zaneschepke.wireguardautotunnel.module
|
|||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class Userspace
|
||||
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Userspace
|
||||
|
|
|
@ -9,17 +9,15 @@ import com.zaneschepke.wireguardautotunnel.util.goAsync
|
|||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@AndroidEntryPoint
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject
|
||||
lateinit var settingsRepository: SettingsRepository
|
||||
@Inject lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) = goAsync {
|
||||
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
|
||||
if (settingsRepository.getSettings().isAutoTunnelEnabled) {
|
||||
ServiceManager.startWatcherServiceForeground(context!!)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -14,13 +14,9 @@ import javax.inject.Inject
|
|||
|
||||
@AndroidEntryPoint
|
||||
class NotificationActionReceiver : BroadcastReceiver() {
|
||||
@Inject
|
||||
lateinit var settingsRepository: SettingsRepository
|
||||
@Inject lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
override fun onReceive(
|
||||
context: Context,
|
||||
intent: Intent?
|
||||
) = goAsync {
|
||||
override fun onReceive(context: Context, intent: Intent?) = goAsync {
|
||||
try {
|
||||
val settings = settingsRepository.getSettings()
|
||||
if (settings.defaultTunnel != null) {
|
||||
|
|
|
@ -15,18 +15,14 @@ open class ForegroundService : LifecycleService() {
|
|||
return null
|
||||
}
|
||||
|
||||
override fun onStartCommand(
|
||||
intent: Intent?,
|
||||
flags: Int,
|
||||
startId: Int
|
||||
): Int {
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
Timber.d("onStartCommand executed with startId: $startId")
|
||||
if (intent != null) {
|
||||
val action = intent.action
|
||||
Timber.d("using an intent with action $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)
|
||||
"android.net.VpnService" -> {
|
||||
Timber.d("Always-on VPN starting service")
|
||||
|
@ -36,7 +32,7 @@ open class ForegroundService : LifecycleService() {
|
|||
}
|
||||
} else {
|
||||
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
|
||||
|
|
|
@ -1,28 +1,27 @@
|
|||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Context.ACTIVITY_SERVICE
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import timber.log.Timber
|
||||
|
||||
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(
|
||||
context: Context,
|
||||
cls: Class<T>
|
||||
): ServiceState {
|
||||
val isServiceRunning = context.isServiceRunning(cls)
|
||||
return if (isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED
|
||||
}
|
||||
// private
|
||||
// fun <T> Context.isServiceRunning(service: Class<T>) =
|
||||
// (getSystemService(ACTIVITY_SERVICE) as ActivityManager)
|
||||
// .runningAppProcesses.any {
|
||||
// it.processName == service.name
|
||||
// }
|
||||
//
|
||||
// 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(
|
||||
action: Action,
|
||||
|
@ -30,14 +29,10 @@ object ServiceManager {
|
|||
cls: Class<T>,
|
||||
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 =
|
||||
Intent(context, cls).also {
|
||||
it.action = action.name
|
||||
extras?.forEach { (k, v) ->
|
||||
it.putExtra(k, v)
|
||||
}
|
||||
extras?.forEach { (k, v) -> it.putExtra(k, v) }
|
||||
}
|
||||
intent.component?.javaClass
|
||||
try {
|
||||
|
@ -45,11 +40,9 @@ object ServiceManager {
|
|||
Action.START_FOREGROUND -> {
|
||||
context.startForegroundService(intent)
|
||||
}
|
||||
|
||||
Action.START -> {
|
||||
context.startService(intent)
|
||||
}
|
||||
|
||||
Action.STOP -> context.startService(intent)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
@ -57,35 +50,30 @@ object ServiceManager {
|
|||
}
|
||||
}
|
||||
|
||||
fun startVpnService(
|
||||
context: Context,
|
||||
tunnelConfig: String
|
||||
) {
|
||||
fun startVpnService(context: Context, tunnelConfig: String) {
|
||||
actionOnService(
|
||||
Action.START,
|
||||
context,
|
||||
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) {
|
||||
Timber.d("Stopping vpn service action")
|
||||
actionOnService(
|
||||
Action.STOP,
|
||||
context,
|
||||
WireGuardTunnelService::class.java
|
||||
WireGuardTunnelService::class.java,
|
||||
)
|
||||
}
|
||||
|
||||
fun startVpnServiceForeground(
|
||||
context: Context,
|
||||
tunnelConfig: String
|
||||
) {
|
||||
fun startVpnServiceForeground(context: Context, tunnelConfig: String) {
|
||||
actionOnService(
|
||||
Action.START_FOREGROUND,
|
||||
context,
|
||||
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(
|
||||
Action.START_FOREGROUND,
|
||||
context,
|
||||
WireGuardConnectivityWatcherService::class.java
|
||||
WireGuardConnectivityWatcherService::class.java,
|
||||
)
|
||||
}
|
||||
|
||||
fun startWatcherService(
|
||||
context: Context
|
||||
) {
|
||||
fun startWatcherService(context: Context) {
|
||||
actionOnService(
|
||||
Action.START,
|
||||
context,
|
||||
WireGuardConnectivityWatcherService::class.java
|
||||
WireGuardConnectivityWatcherService::class.java,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -113,7 +99,7 @@ object ServiceManager {
|
|||
actionOnService(
|
||||
Action.STOP,
|
||||
context,
|
||||
WireGuardConnectivityWatcherService::class.java
|
||||
WireGuardConnectivityWatcherService::class.java,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,6 +49,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||
@Inject lateinit var vpnService: VpnService
|
||||
|
||||
private val networkEventsFlow = MutableStateFlow(WatcherState())
|
||||
|
||||
data class WatcherState(
|
||||
val isWifiConnected: Boolean = false,
|
||||
val isVpnConnected: Boolean = false,
|
||||
|
@ -99,15 +100,22 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||
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 =
|
||||
notificationService.createNotification(
|
||||
channelId = getString(R.string.watcher_channel_id),
|
||||
channelName = getString(R.string.watcher_channel_name),
|
||||
title = getString(R.string.auto_tunnel_title),
|
||||
description = description)
|
||||
description = description,
|
||||
)
|
||||
ServiceCompat.startForeground(
|
||||
this, foregroundId, notification, Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID)
|
||||
this,
|
||||
foregroundId,
|
||||
notification,
|
||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
||||
)
|
||||
}
|
||||
|
||||
private fun launchWatcherPausedNotification() {
|
||||
|
@ -124,14 +132,16 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||
this,
|
||||
1,
|
||||
restartServiceIntent,
|
||||
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE)
|
||||
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
applicationContext.getSystemService(Context.ALARM_SERVICE)
|
||||
val alarmService: AlarmManager =
|
||||
applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
alarmService.set(
|
||||
AlarmManager.ELAPSED_REALTIME,
|
||||
SystemClock.elapsedRealtime() + 1000,
|
||||
restartServicePendingIntent)
|
||||
restartServicePendingIntent,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun initWakeLock() {
|
||||
|
@ -199,25 +209,29 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||
when (it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.d("Gained Mobile data connection")
|
||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
||||
isMobileDataConnected = true
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isMobileDataConnected = true,
|
||||
)
|
||||
}
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
||||
isMobileDataConnected = true
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isMobileDataConnected = true,
|
||||
)
|
||||
Timber.d("Mobile data capabilities changed")
|
||||
}
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
||||
isMobileDataConnected = false
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isMobileDataConnected = false,
|
||||
)
|
||||
Timber.d("Lost mobile data connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun watchForSettingsChanges() {
|
||||
settingsRepository.getSettingsFlow().collect {
|
||||
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
|
||||
|
@ -226,8 +240,9 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||
false -> launchWatcherNotification()
|
||||
}
|
||||
}
|
||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
||||
settings = it
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
settings = it,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -235,11 +250,15 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||
private suspend fun watchForVpnConnectivityChanges() {
|
||||
vpnService.vpnState.collect {
|
||||
when (it.status) {
|
||||
Tunnel.State.DOWN -> networkEventsFlow.value = networkEventsFlow.value.copy(
|
||||
isVpnConnected = false
|
||||
Tunnel.State.DOWN ->
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isVpnConnected = false,
|
||||
)
|
||||
Tunnel.State.UP -> networkEventsFlow.value = networkEventsFlow.value.copy(
|
||||
isVpnConnected = true
|
||||
Tunnel.State.UP ->
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isVpnConnected = true,
|
||||
)
|
||||
else -> {}
|
||||
}
|
||||
|
@ -251,19 +270,22 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||
when (it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.d("Gained Ethernet connection")
|
||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
||||
isEthernetConnected = true
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isEthernetConnected = true,
|
||||
)
|
||||
}
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.d("Ethernet capabilities changed")
|
||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
||||
isEthernetConnected = true
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isEthernetConnected = true,
|
||||
)
|
||||
}
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
||||
isEthernetConnected = false
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isEthernetConnected = false,
|
||||
)
|
||||
Timber.d("Lost Ethernet connection")
|
||||
}
|
||||
|
@ -276,24 +298,28 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||
when (it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.d("Gained Wi-Fi connection")
|
||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
||||
isWifiConnected = true
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isWifiConnected = true,
|
||||
)
|
||||
}
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.d("Wifi capabilities changed")
|
||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
||||
isWifiConnected = true
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isWifiConnected = true,
|
||||
)
|
||||
val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: ""
|
||||
Timber.d("Detected SSID: $ssid")
|
||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
||||
currentNetworkSSID = ssid
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
currentNetworkSSID = ssid,
|
||||
)
|
||||
}
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
||||
isWifiConnected = false
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isWifiConnected = false,
|
||||
)
|
||||
Timber.d("Lost Wi-Fi connection")
|
||||
}
|
||||
|
@ -338,7 +364,8 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||
Timber.i("Condition 4 met")
|
||||
}
|
||||
(!it.isEthernetConnected &&
|
||||
(it.isWifiConnected && it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) &&
|
||||
(it.isWifiConnected &&
|
||||
it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) &&
|
||||
(it.isVpnConnected)) -> {
|
||||
ServiceManager.stopVpnService(this)
|
||||
Timber.i("Condition 5 met")
|
||||
|
|
|
@ -28,17 +28,13 @@ import javax.inject.Inject
|
|||
class WireGuardTunnelService : ForegroundService() {
|
||||
private val foregroundId = 123
|
||||
|
||||
@Inject
|
||||
lateinit var vpnService: VpnService
|
||||
@Inject lateinit var vpnService: VpnService
|
||||
|
||||
@Inject
|
||||
lateinit var settingsRepository: SettingsRepository
|
||||
@Inject lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||
|
||||
@Inject
|
||||
lateinit var notificationService: NotificationService
|
||||
@Inject lateinit var notificationService: NotificationService
|
||||
|
||||
private lateinit var job: Job
|
||||
|
||||
|
@ -58,11 +54,10 @@ class WireGuardTunnelService : ForegroundService() {
|
|||
super.startService(extras)
|
||||
cancelJob()
|
||||
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
|
||||
val tunnelConfig = tunnelConfigString?.let {
|
||||
TunnelConfig.from(it)
|
||||
}
|
||||
val tunnelConfig = tunnelConfigString?.let { TunnelConfig.from(it) }
|
||||
tunnelName = tunnelConfig?.name ?: ""
|
||||
job = lifecycleScope.launch(Dispatchers.IO) {
|
||||
job =
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
launch {
|
||||
if (tunnelConfig != null) {
|
||||
try {
|
||||
|
@ -77,7 +72,8 @@ class WireGuardTunnelService : ForegroundService() {
|
|||
val settings = settingsRepository.getSettings()
|
||||
val tunnels = tunnelConfigRepository.getAll()
|
||||
if (settings.isAlwaysOnVpnEnabled) {
|
||||
val tunnel = if(settings.defaultTunnel != null) {
|
||||
val tunnel =
|
||||
if (settings.defaultTunnel != null) {
|
||||
TunnelConfig.from(settings.defaultTunnel!!)
|
||||
} else if (tunnels.isNotEmpty()) {
|
||||
tunnels.first()
|
||||
|
@ -88,7 +84,6 @@ class WireGuardTunnelService : ForegroundService() {
|
|||
tunnelName = tunnel.name
|
||||
vpnService.startTunnel(tunnel)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -103,12 +98,16 @@ class WireGuardTunnelService : ForegroundService() {
|
|||
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
|
||||
if (!didShowConnected) {
|
||||
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
|
||||
launchVpnNotification(getString(R.string.tunnel_start_title),"${getString(R.string.tunnel_start_text)} $tunnelName")
|
||||
launchVpnNotification(
|
||||
getString(R.string.tunnel_start_title),
|
||||
"${getString(R.string.tunnel_start_text)} $tunnelName",
|
||||
)
|
||||
didShowConnected = true
|
||||
}
|
||||
}
|
||||
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
|
||||
statuses?.all { it == HandshakeStatus.NOT_STARTED } == true -> {}
|
||||
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
|
||||
true -> {}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
@ -127,7 +126,10 @@ class WireGuardTunnelService : ForegroundService() {
|
|||
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 =
|
||||
notificationService.createNotification(
|
||||
channelId = getString(R.string.vpn_channel_id),
|
||||
|
@ -136,13 +138,13 @@ class WireGuardTunnelService : ForegroundService() {
|
|||
onGoing = false,
|
||||
vibration = false,
|
||||
showTimestamp = true,
|
||||
description = description
|
||||
description = description,
|
||||
)
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
foregroundId,
|
||||
notification,
|
||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID
|
||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -156,20 +158,20 @@ class WireGuardTunnelService : ForegroundService() {
|
|||
this,
|
||||
0,
|
||||
Intent(this, NotificationActionReceiver::class.java),
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
PendingIntent.FLAG_IMMUTABLE,
|
||||
),
|
||||
actionText = getString(R.string.restart),
|
||||
title = getString(R.string.vpn_connection_failed),
|
||||
onGoing = false,
|
||||
vibration = true,
|
||||
showTimestamp = true,
|
||||
description = message
|
||||
description = message,
|
||||
)
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
foregroundId,
|
||||
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 =
|
||||
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
||||
|
||||
override val networkStatus =
|
||||
callbackFlow {
|
||||
override val networkStatus = callbackFlow {
|
||||
val networkStatusCallback =
|
||||
when (Build.VERSION.SDK_INT) {
|
||||
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
|
||||
object : ConnectivityManager.NetworkCallback(
|
||||
FLAG_INCLUDE_LOCATION_INFO
|
||||
object :
|
||||
ConnectivityManager.NetworkCallback(
|
||||
FLAG_INCLUDE_LOCATION_INFO,
|
||||
) {
|
||||
override fun onAvailable(network: Network) {
|
||||
trySend(NetworkStatus.Available(network))
|
||||
|
@ -47,13 +47,12 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
|||
trySend(
|
||||
NetworkStatus.CapabilitiesChanged(
|
||||
network,
|
||||
networkCapabilities
|
||||
)
|
||||
networkCapabilities,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
|
@ -71,8 +70,8 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
|||
trySend(
|
||||
NetworkStatus.CapabilitiesChanged(
|
||||
network,
|
||||
networkCapabilities
|
||||
)
|
||||
networkCapabilities,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -86,9 +85,7 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
|||
.build()
|
||||
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
|
||||
|
||||
awaitClose {
|
||||
connectivityManager.unregisterNetworkCallback(networkStatusCallback)
|
||||
}
|
||||
awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) }
|
||||
}
|
||||
|
||||
override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? {
|
||||
|
@ -119,18 +116,16 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
|||
inline fun <Result> Flow<NetworkStatus>.map(
|
||||
crossinline onUnavailable: suspend (network: Network) -> Result,
|
||||
crossinline onAvailable: suspend (network: Network) -> Result,
|
||||
crossinline onCapabilitiesChanged: suspend (
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities
|
||||
) -> Result
|
||||
): Flow<Result> =
|
||||
map { status ->
|
||||
crossinline onCapabilitiesChanged:
|
||||
suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result
|
||||
): Flow<Result> = map { status ->
|
||||
when (status) {
|
||||
is NetworkStatus.Unavailable -> onUnavailable(status.network)
|
||||
is NetworkStatus.Available -> onAvailable(status.network)
|
||||
is NetworkStatus.CapabilitiesChanged -> onCapabilitiesChanged(
|
||||
is NetworkStatus.CapabilitiesChanged ->
|
||||
onCapabilitiesChanged(
|
||||
status.network,
|
||||
status.networkCapabilities
|
||||
status.networkCapabilities,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,9 +5,5 @@ import android.net.NetworkCapabilities
|
|||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class EthernetService
|
||||
@Inject
|
||||
constructor(
|
||||
@ApplicationContext context: Context
|
||||
) :
|
||||
class EthernetService @Inject constructor(@ApplicationContext context: Context) :
|
||||
BaseNetworkService<EthernetService>(context, NetworkCapabilities.TRANSPORT_ETHERNET)
|
||||
|
|
|
@ -5,9 +5,5 @@ import android.net.NetworkCapabilities
|
|||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class MobileDataService
|
||||
@Inject
|
||||
constructor(
|
||||
@ApplicationContext context: Context
|
||||
) :
|
||||
class MobileDataService @Inject constructor(@ApplicationContext context: Context) :
|
||||
BaseNetworkService<MobileDataService>(context, NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
|
|
|
@ -5,9 +5,5 @@ import android.net.NetworkCapabilities
|
|||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class WifiService
|
||||
@Inject
|
||||
constructor(
|
||||
@ApplicationContext context: Context
|
||||
) :
|
||||
class WifiService @Inject constructor(@ApplicationContext context: Context) :
|
||||
BaseNetworkService<WifiService>(context, NetworkCapabilities.TRANSPORT_WIFI)
|
||||
|
|
|
@ -3,7 +3,6 @@ package com.zaneschepke.wireguardautotunnel.service.notification
|
|||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import androidx.core.app.NotificationCompat
|
||||
|
||||
interface NotificationService {
|
||||
fun createNotification(
|
||||
|
|
|
@ -13,22 +13,20 @@ import com.zaneschepke.wireguardautotunnel.ui.MainActivity
|
|||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class WireGuardNotification
|
||||
@Inject
|
||||
constructor(
|
||||
@ApplicationContext private val context: Context
|
||||
) : NotificationService {
|
||||
class WireGuardNotification @Inject constructor(@ApplicationContext private val context: Context) :
|
||||
NotificationService {
|
||||
private val notificationManager =
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
private val watcherBuilder: NotificationCompat.Builder =
|
||||
NotificationCompat.Builder(
|
||||
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.getString(R.string.vpn_channel_id)
|
||||
context.getString(R.string.vpn_channel_id),
|
||||
)
|
||||
|
||||
override fun createNotification(
|
||||
|
@ -49,8 +47,9 @@ constructor(
|
|||
NotificationChannel(
|
||||
channelId,
|
||||
channelName,
|
||||
importance
|
||||
).let {
|
||||
importance,
|
||||
)
|
||||
.let {
|
||||
it.description = title
|
||||
it.enableLights(lights)
|
||||
it.lightColor = Color.RED
|
||||
|
@ -65,17 +64,18 @@ constructor(
|
|||
context,
|
||||
0,
|
||||
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.vpn_channel_id) -> tunnelBuilder
|
||||
else -> {
|
||||
NotificationCompat.Builder(
|
||||
context,
|
||||
channelId
|
||||
channelId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -83,8 +83,7 @@ constructor(
|
|||
return builder.let {
|
||||
if (action != null && actionText != null) {
|
||||
it.addAction(
|
||||
NotificationCompat.Action.Builder(0, actionText, action)
|
||||
.build()
|
||||
NotificationCompat.Action.Builder(0, actionText, action).build(),
|
||||
)
|
||||
it.setAutoCancel(true)
|
||||
}
|
||||
|
|
|
@ -12,33 +12,34 @@ import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
|||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ShortcutsActivity : ComponentActivity() {
|
||||
@Inject
|
||||
lateinit var settingsRepository: SettingsRepository
|
||||
@Inject lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||
|
||||
private suspend fun toggleWatcherServicePause() {
|
||||
val settings = settingsRepository.getSettings()
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
||||
settingsRepository.save(settings.copy(
|
||||
isAutoTunnelPaused = pauseAutoTunnel
|
||||
))
|
||||
settingsRepository.save(
|
||||
settings.copy(
|
||||
isAutoTunnelPaused = pauseAutoTunnel,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(View(this))
|
||||
if (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)
|
||||
if (
|
||||
intent
|
||||
.getStringExtra(CLASS_NAME_EXTRA_KEY)
|
||||
.equals(WireGuardTunnelService::class.java.simpleName)
|
||||
) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
|
@ -48,7 +49,9 @@ class ShortcutsActivity : ComponentActivity() {
|
|||
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
|
||||
val tunnelConfig =
|
||||
if (tunnelName != null) {
|
||||
tunnelConfigRepository.getAll().firstOrNull { it.name == tunnelName }
|
||||
tunnelConfigRepository.getAll().firstOrNull {
|
||||
it.name == tunnelName
|
||||
}
|
||||
} else {
|
||||
if (settings.defaultTunnel == null) {
|
||||
tunnelConfigRepository.getAll().first()
|
||||
|
@ -59,12 +62,14 @@ class ShortcutsActivity : ComponentActivity() {
|
|||
tunnelConfig ?: return@launch
|
||||
toggleWatcherServicePause()
|
||||
when (intent.action) {
|
||||
Action.STOP.name -> ServiceManager.stopVpnService(
|
||||
this@ShortcutsActivity
|
||||
)
|
||||
Action.START.name -> ServiceManager.startVpnServiceForeground(
|
||||
Action.STOP.name ->
|
||||
ServiceManager.stopVpnService(
|
||||
this@ShortcutsActivity,
|
||||
tunnelConfig.toString()
|
||||
)
|
||||
Action.START.name ->
|
||||
ServiceManager.startVpnServiceForeground(
|
||||
this@ShortcutsActivity,
|
||||
tunnelConfig.toString(),
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
|
|
@ -20,14 +20,11 @@ import javax.inject.Inject
|
|||
@AndroidEntryPoint
|
||||
class TunnelControlTile() : TileService() {
|
||||
|
||||
@Inject
|
||||
lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||
|
||||
@Inject
|
||||
lateinit var settingsRepository: SettingsRepository
|
||||
@Inject lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var vpnService: VpnService
|
||||
@Inject lateinit var vpnService: VpnService
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
|
@ -48,7 +45,8 @@ class TunnelControlTile() : TileService() {
|
|||
setUnavailable()
|
||||
return@collect
|
||||
}
|
||||
tunnelName = it.name.ifBlank {
|
||||
tunnelName =
|
||||
it.name.ifBlank {
|
||||
val settings = settingsRepository.getSettings()
|
||||
if (settings.defaultTunnel != null) {
|
||||
TunnelConfig.from(settings.defaultTunnel!!).name
|
||||
|
@ -58,6 +56,7 @@ class TunnelControlTile() : TileService() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
scope.cancel()
|
||||
|
@ -73,14 +72,15 @@ class TunnelControlTile() : TileService() {
|
|||
unlockAndRun {
|
||||
scope.launch {
|
||||
try {
|
||||
val tunnelConfig = tunnelConfigRepository.getAll().first { it.name == tunnelName }
|
||||
val tunnelConfig =
|
||||
tunnelConfigRepository.getAll().first { it.name == tunnelName }
|
||||
toggleWatcherServicePause()
|
||||
if (vpnService.getState() == Tunnel.State.UP) {
|
||||
ServiceManager.stopVpnService(this@TunnelControlTile)
|
||||
} else {
|
||||
ServiceManager.startVpnServiceForeground(
|
||||
this@TunnelControlTile,
|
||||
tunnelConfig.toString()
|
||||
tunnelConfig.toString(),
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
@ -97,9 +97,11 @@ class TunnelControlTile() : TileService() {
|
|||
val settings = settingsRepository.getSettings()
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
||||
settingsRepository.save(settings.copy(
|
||||
isAutoTunnelPaused = pauseAutoTunnel
|
||||
))
|
||||
settingsRepository.save(
|
||||
settings.copy(
|
||||
isAutoTunnelPaused = pauseAutoTunnel,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,13 +4,13 @@ enum class HandshakeStatus {
|
|||
HEALTHY,
|
||||
STALE,
|
||||
UNKNOWN,
|
||||
NOT_STARTED
|
||||
;
|
||||
NOT_STARTED;
|
||||
|
||||
companion object {
|
||||
private const val WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC = 180
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ constructor(
|
|||
backend.setState(
|
||||
this,
|
||||
State.UP,
|
||||
config
|
||||
config,
|
||||
)
|
||||
emitTunnelState(state)
|
||||
state
|
||||
|
@ -80,24 +80,24 @@ constructor(
|
|||
private fun emitTunnelState(state: State) {
|
||||
_vpnState.tryEmit(
|
||||
_vpnState.value.copy(
|
||||
status = state
|
||||
)
|
||||
status = state,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun emitBackendStatistics(statistics: Statistics) {
|
||||
_vpnState.tryEmit(
|
||||
_vpnState.value.copy(
|
||||
statistics = statistics
|
||||
)
|
||||
statistics = statistics,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun emitTunnelName(name: String) {
|
||||
_vpnState.emit(
|
||||
_vpnState.value.copy(
|
||||
name = name
|
||||
)
|
||||
name = name,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -6,8 +6,8 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ActivityViewModel @Inject constructor(
|
||||
class ActivityViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
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.focusProperties
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
|
@ -37,6 +38,9 @@ import com.google.accompanist.permissions.isGranted
|
|||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
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.navigation.BottomNavBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
|
||||
|
@ -50,14 +54,35 @@ import com.zaneschepke.wireguardautotunnel.util.Constants
|
|||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var dataStoreManager: DataStoreManager
|
||||
|
||||
@Inject lateinit var settingsRepository: SettingsRepository
|
||||
@OptIn(
|
||||
ExperimentalPermissionsApi::class
|
||||
ExperimentalPermissionsApi::class,
|
||||
)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
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 {
|
||||
// val activityViewModel = hiltViewModel<ActivityViewModel>()
|
||||
|
||||
|
@ -73,7 +98,10 @@ class MainActivity : AppCompatActivity() {
|
|||
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -87,7 +115,7 @@ class MainActivity : AppCompatActivity() {
|
|||
if (accepted) {
|
||||
vpnIntent = null
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
LaunchedEffect(vpnIntent) {
|
||||
if (vpnIntent != null) {
|
||||
|
@ -99,13 +127,15 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
fun showSnackBarMessage(message: String) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
val result =
|
||||
snackbarHostState.showSnackbar(
|
||||
message = message,
|
||||
actionLabel = applicationContext.getString(R.string.okay),
|
||||
duration = SnackbarDuration.Short
|
||||
duration = SnackbarDuration.Short,
|
||||
)
|
||||
when (result) {
|
||||
SnackbarResult.ActionPerformed, SnackbarResult.Dismissed -> {
|
||||
SnackbarResult.ActionPerformed,
|
||||
SnackbarResult.Dismissed -> {
|
||||
snackbarHostState.currentSnackbarData?.dismiss()
|
||||
}
|
||||
}
|
||||
|
@ -118,29 +148,36 @@ class MainActivity : AppCompatActivity() {
|
|||
CustomSnackBar(
|
||||
snackbarData.visuals.message,
|
||||
isRtl = false,
|
||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||
2.dp
|
||||
)
|
||||
containerColor =
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||
2.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.focusable().focusProperties { up = focusRequester },
|
||||
bottomBar =
|
||||
if (vpnIntent == null && notificationPermissionState.status.isGranted) {
|
||||
{ BottomNavBar(navController, listOf(
|
||||
{
|
||||
BottomNavBar(
|
||||
navController,
|
||||
listOf(
|
||||
Screen.Main.navItem,
|
||||
Screen.Settings.navItem,
|
||||
Screen.Support.navItem)) }
|
||||
Screen.Support.navItem,
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
{}
|
||||
}
|
||||
},
|
||||
) { padding ->
|
||||
if (vpnIntent != null) {
|
||||
PermissionRequestFailedScreen(
|
||||
padding = padding,
|
||||
onRequestAgain = { vpnActivityResultState.launch(vpnIntent) },
|
||||
message = getString(R.string.vpn_permission_required),
|
||||
getString(R.string.retry)
|
||||
getString(R.string.retry),
|
||||
)
|
||||
return@Scaffold
|
||||
}
|
||||
|
@ -154,12 +191,12 @@ class MainActivity : AppCompatActivity() {
|
|||
Uri.fromParts(
|
||||
Constants.URI_PACKAGE_SCHEME,
|
||||
this.packageName,
|
||||
null
|
||||
null,
|
||||
)
|
||||
startActivity(intentSettings)
|
||||
},
|
||||
message = getString(R.string.notification_permission_required),
|
||||
getString(R.string.open_settings)
|
||||
getString(R.string.open_settings),
|
||||
)
|
||||
return@Scaffold
|
||||
}
|
||||
|
@ -167,22 +204,30 @@ class MainActivity : AppCompatActivity() {
|
|||
composable(
|
||||
Screen.Main.route,
|
||||
) {
|
||||
MainScreen(padding = padding, focusRequester = focusRequester, showSnackbarMessage = { message ->
|
||||
showSnackBarMessage(message)
|
||||
}, navController = navController)
|
||||
MainScreen(
|
||||
padding = padding,
|
||||
focusRequester = focusRequester,
|
||||
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
||||
navController = navController,
|
||||
)
|
||||
}
|
||||
composable(Screen.Settings.route,
|
||||
composable(
|
||||
Screen.Settings.route,
|
||||
) {
|
||||
SettingsScreen(padding = padding, showSnackbarMessage = { message ->
|
||||
showSnackBarMessage(message)
|
||||
}, focusRequester = focusRequester)
|
||||
SettingsScreen(
|
||||
padding = padding,
|
||||
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
||||
focusRequester = focusRequester,
|
||||
)
|
||||
}
|
||||
composable(Screen.Support.route,
|
||||
composable(
|
||||
Screen.Support.route,
|
||||
) {
|
||||
SupportScreen(padding = padding, focusRequester = focusRequester,
|
||||
showSnackbarMessage = { message ->
|
||||
showSnackBarMessage(message)
|
||||
})
|
||||
SupportScreen(
|
||||
padding = padding,
|
||||
focusRequester = focusRequester,
|
||||
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
||||
)
|
||||
}
|
||||
composable("${Screen.Config.route}/{id}") {
|
||||
val id = it.arguments?.getString("id")
|
||||
|
@ -194,7 +239,7 @@ class MainActivity : AppCompatActivity() {
|
|||
showSnackbarMessage = { message ->
|
||||
showSnackBarMessage(message)
|
||||
},
|
||||
focusRequester = focusRequester
|
||||
focusRequester = focusRequester,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,26 +8,31 @@ import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
|
|||
|
||||
sealed class Screen(val route: String) {
|
||||
data object Main : Screen("main") {
|
||||
val navItem = BottomNavItem(
|
||||
val navItem =
|
||||
BottomNavItem(
|
||||
name = "Tunnels",
|
||||
route = route,
|
||||
icon = Icons.Rounded.Home
|
||||
icon = Icons.Rounded.Home,
|
||||
)
|
||||
}
|
||||
|
||||
data object Settings : Screen("settings") {
|
||||
val navItem = BottomNavItem(
|
||||
val navItem =
|
||||
BottomNavItem(
|
||||
name = "Settings",
|
||||
route = route,
|
||||
icon = Icons.Rounded.Settings
|
||||
icon = Icons.Rounded.Settings,
|
||||
)
|
||||
}
|
||||
|
||||
data object Support : Screen("support") {
|
||||
val navItem = BottomNavItem(
|
||||
val navItem =
|
||||
BottomNavItem(
|
||||
name = "Support",
|
||||
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(
|
||||
onClick = onClick,
|
||||
enabled = enabled
|
||||
enabled = enabled,
|
||||
) {
|
||||
Text(text, Modifier.weight(1f, false))
|
||||
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
|
||||
|
@ -35,7 +35,7 @@ fun ClickableIconButton(
|
|||
if (enabled) {
|
||||
onIconClick()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,17 +26,12 @@ fun PermissionRequestFailedScreen(
|
|||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
modifier = Modifier.fillMaxSize().padding(padding),
|
||||
) {
|
||||
Text(message, textAlign = TextAlign.Center, modifier = Modifier.padding(15.dp))
|
||||
Button(onClick = {
|
||||
scope.launch {
|
||||
onRequestAgain()
|
||||
}
|
||||
}) {
|
||||
Button(
|
||||
onClick = { scope.launch { onRequestAgain() } },
|
||||
) {
|
||||
Text(buttonText)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,30 +34,22 @@ fun RowListItem(
|
|||
) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.animateContentSize()
|
||||
Modifier.animateContentSize()
|
||||
.clip(RoundedCornerShape(30.dp))
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
onClick()
|
||||
},
|
||||
onLongClick = {
|
||||
onHold()
|
||||
}
|
||||
)
|
||||
onClick = { onClick() },
|
||||
onLongClick = { onHold() },
|
||||
),
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 15.dp, vertical = 5.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 15.dp, vertical = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(.60f)
|
||||
modifier = Modifier.fillMaxWidth(.60f),
|
||||
) {
|
||||
icon()
|
||||
Text(text)
|
||||
|
@ -68,11 +60,10 @@ fun RowListItem(
|
|||
statistics?.peers()?.forEach {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
Modifier.fillMaxWidth()
|
||||
.padding(end = 10.dp, bottom = 10.dp, start = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
|
||||
val peerTx = statistics.peer(it)!!.txBytes
|
||||
|
|
|
@ -47,7 +47,7 @@ fun SearchBar(onQuery: (queryString: String) -> Unit) {
|
|||
Icon(
|
||||
imageVector = Icons.Rounded.Search,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
contentDescription = stringResource(id = R.string.search_icon)
|
||||
contentDescription = stringResource(id = R.string.search_icon),
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
|
@ -56,7 +56,7 @@ fun SearchBar(onQuery: (queryString: String) -> Unit) {
|
|||
Icon(
|
||||
imageVector = Icons.Rounded.Clear,
|
||||
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(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
disabledContainerColor = Color.Transparent
|
||||
disabledContainerColor = Color.Transparent,
|
||||
),
|
||||
placeholder = { Text(text = stringResource(R.string.hint_search_packages)) },
|
||||
textStyle = MaterialTheme.typography.bodySmall,
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape)
|
||||
Modifier.fillMaxWidth()
|
||||
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -22,19 +22,15 @@ fun ConfigurationTextBox(
|
|||
modifier = modifier,
|
||||
value = value,
|
||||
singleLine = true,
|
||||
onValueChange = {
|
||||
onValueChange(it)
|
||||
},
|
||||
onValueChange = { onValueChange(it) },
|
||||
label = { Text(label) },
|
||||
maxLines = 1,
|
||||
placeholder = {
|
||||
Text(hint)
|
||||
},
|
||||
placeholder = { Text(hint) },
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
imeAction = ImeAction.Done
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
keyboardActions = keyboardActions
|
||||
keyboardActions = keyboardActions,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -21,21 +21,16 @@ fun ConfigurationToggle(
|
|||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(padding),
|
||||
modifier = Modifier.fillMaxWidth().padding(padding),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(label)
|
||||
Switch(
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
checked = checked,
|
||||
onCheckedChange = {
|
||||
onCheckChanged()
|
||||
}
|
||||
onCheckedChange = { onCheckChanged() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,14 +11,11 @@ import androidx.navigation.NavController
|
|||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
|
||||
@Composable
|
||||
fun BottomNavBar(
|
||||
navController: NavController,
|
||||
bottomNavItems: List<BottomNavItem>
|
||||
) {
|
||||
fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) {
|
||||
val backStackEntry = navController.currentBackStackEntryAsState()
|
||||
|
||||
NavigationBar(
|
||||
containerColor = MaterialTheme.colorScheme.background
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
) {
|
||||
bottomNavItems.forEach { item ->
|
||||
val selected = item.route == backStackEntry.value?.destination?.route
|
||||
|
@ -29,15 +26,15 @@ fun BottomNavBar(
|
|||
label = {
|
||||
Text(
|
||||
text = item.name,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
},
|
||||
icon = {
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun AuthorizationPrompt(
|
||||
onSuccess: () -> Unit,
|
||||
onFailure: () -> Unit,
|
||||
onError: (String) -> Unit
|
||||
) {
|
||||
fun AuthorizationPrompt(onSuccess: () -> Unit, onFailure: () -> Unit, onError: (String) -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val biometricManager = BiometricManager.from(context)
|
||||
val bio = biometricManager.canAuthenticate(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
|
||||
val isBiometricAvailable =
|
||||
remember {
|
||||
val isBiometricAvailable = remember {
|
||||
when (bio) {
|
||||
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
|
||||
onError("Biometrics not available")
|
||||
false
|
||||
}
|
||||
|
||||
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
|
||||
onError("Biometrics not created")
|
||||
false
|
||||
}
|
||||
|
||||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
|
||||
onError("Biometric hardware not found")
|
||||
false
|
||||
}
|
||||
|
||||
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
|
||||
onError("Biometric security update required")
|
||||
false
|
||||
}
|
||||
|
||||
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
|
||||
onError("Biometrics not supported")
|
||||
false
|
||||
}
|
||||
|
||||
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
|
||||
onError("Biometrics status unknown")
|
||||
false
|
||||
}
|
||||
|
||||
BiometricManager.BIOMETRIC_SUCCESS -> true
|
||||
else -> false
|
||||
}
|
||||
|
@ -71,10 +60,7 @@ fun AuthorizationPrompt(
|
|||
context as FragmentActivity,
|
||||
executor,
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationError(
|
||||
errorCode: Int,
|
||||
errString: CharSequence
|
||||
) {
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
onFailure()
|
||||
}
|
||||
|
@ -90,7 +76,7 @@ fun AuthorizationPrompt(
|
|||
super.onAuthenticationFailed()
|
||||
onFailure()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
biometricPrompt.authenticate(promptInfo)
|
||||
}
|
||||
|
|
|
@ -38,24 +38,24 @@ fun CustomSnackBar(
|
|||
containerColor = containerColor,
|
||||
modifier =
|
||||
Modifier.fillMaxWidth(
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f
|
||||
).padding(bottom = 100.dp),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f,
|
||||
)
|
||||
.padding(bottom = 100.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalLayoutDirection provides
|
||||
if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
|
||||
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.width(IntrinsicSize.Max).height(IntrinsicSize.Min),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
) {
|
||||
Icon(
|
||||
Icons.Rounded.Info,
|
||||
contentDescription = stringResource(R.string.info),
|
||||
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))
|
||||
}
|
||||
|
|
|
@ -16,7 +16,8 @@ fun LoadingScreen() {
|
|||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.fillMaxSize().focusable().padding()) {
|
||||
modifier = Modifier.fillMaxSize().focusable().padding(),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,14 +12,11 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
fun SectionTitle(
|
||||
title: String,
|
||||
padding: Dp
|
||||
) {
|
||||
fun SectionTitle(title: String, padding: Dp) {
|
||||
Text(
|
||||
title,
|
||||
textAlign = TextAlign.Center,
|
||||
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(),
|
||||
addresses = i.addresses.joinToString(", ").trim(),
|
||||
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
|
||||
listenPort = if (i.listenPort.isPresent) {
|
||||
i.listenPort.get().toString()
|
||||
.trim()
|
||||
listenPort =
|
||||
if (i.listenPort.isPresent) {
|
||||
i.listenPort.get().toString().trim()
|
||||
} 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 {
|
||||
return PeerProxy(
|
||||
publicKey = peer.publicKey.toBase64(),
|
||||
preSharedKey = if (peer.preSharedKey.isPresent) {
|
||||
peer.preSharedKey.get().toBase64()
|
||||
.trim()
|
||||
preSharedKey =
|
||||
if (peer.preSharedKey.isPresent) {
|
||||
peer.preSharedKey.get().toBase64().trim()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
persistentKeepalive = if (peer.persistentKeepalive.isPresent) {
|
||||
peer.persistentKeepalive.get()
|
||||
.toString().trim()
|
||||
persistentKeepalive =
|
||||
if (peer.persistentKeepalive.isPresent) {
|
||||
peer.persistentKeepalive.get().toString().trim()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
endpoint = if (peer.endpoint.isPresent) {
|
||||
peer.endpoint.get().toString()
|
||||
.trim()
|
||||
endpoint =
|
||||
if (peer.endpoint.isPresent) {
|
||||
peer.endpoint.get().toString().trim()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
allowedIps = peer.allowedIps.joinToString(", ").trim()
|
||||
allowedIps = peer.allowedIps.joinToString(", ").trim(),
|
||||
)
|
||||
}
|
||||
|
||||
val IPV4_PUBLIC_NETWORKS =
|
||||
setOf(
|
||||
"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",
|
||||
"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"
|
||||
"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",
|
||||
"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")
|
||||
}
|
||||
|
|
|
@ -88,7 +88,8 @@ import kotlinx.coroutines.delay
|
|||
@OptIn(
|
||||
ExperimentalComposeUiApi::class,
|
||||
ExperimentalMaterial3Api::class,
|
||||
ExperimentalFoundationApi::class)
|
||||
ExperimentalFoundationApi::class,
|
||||
)
|
||||
@Composable
|
||||
fun ConfigScreen(
|
||||
viewModel: ConfigViewModel = hiltViewModel(),
|
||||
|
@ -106,9 +107,7 @@ fun ConfigScreen(
|
|||
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.init(id)
|
||||
}
|
||||
LaunchedEffect(Unit) { viewModel.init(id) }
|
||||
|
||||
LaunchedEffect(uiState.loading) {
|
||||
if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
|
@ -124,8 +123,7 @@ fun ConfigScreen(
|
|||
|
||||
val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() })
|
||||
|
||||
val keyboardOptions =
|
||||
KeyboardOptions(imeAction = ImeAction.Done)
|
||||
val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done)
|
||||
|
||||
val fillMaxHeight = .85f
|
||||
val fillMaxWidth = .85f
|
||||
|
@ -136,7 +134,8 @@ fun ConfigScreen(
|
|||
if (uiState.isAllApplicationsEnabled) {
|
||||
"all"
|
||||
} 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 = {
|
||||
showAuthPrompt = false
|
||||
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (showApplicationsDialog) {
|
||||
val sortedPackages =
|
||||
remember(uiState.packages) { uiState.packages.sortedBy { viewModel.getPackageLabel(it) } }
|
||||
remember(uiState.packages) {
|
||||
uiState.packages.sortedBy { viewModel.getPackageLabel(it) }
|
||||
}
|
||||
AlertDialog(onDismissRequest = { showApplicationsDialog = false }) {
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
|
@ -167,55 +169,75 @@ fun ConfigScreen(
|
|||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f)) {
|
||||
.fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f),
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp),
|
||||
modifier =
|
||||
Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(stringResource(id = R.string.tunnel_all))
|
||||
Switch(
|
||||
checked = uiState.isAllApplicationsEnabled,
|
||||
onCheckedChange = { viewModel.onAllApplicationsChange(it) })
|
||||
onCheckedChange = { viewModel.onAllApplicationsChange(it) },
|
||||
)
|
||||
}
|
||||
if (!uiState.isAllApplicationsEnabled) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp),
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 7.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(stringResource(id = R.string.include))
|
||||
Checkbox(
|
||||
checked = uiState.include,
|
||||
onCheckedChange = { viewModel.onIncludeChange(!uiState.include) })
|
||||
onCheckedChange = {
|
||||
viewModel.onIncludeChange(!uiState.include)
|
||||
},
|
||||
)
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(stringResource(id = R.string.exclude))
|
||||
Checkbox(
|
||||
checked = !uiState.include,
|
||||
onCheckedChange = { viewModel.onIncludeChange(!uiState.include) })
|
||||
onCheckedChange = {
|
||||
viewModel.onIncludeChange(!uiState.include)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp),
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 7.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
SearchBar(viewModel::emitQueriedPackages)
|
||||
}
|
||||
Spacer(Modifier.padding(5.dp))
|
||||
LazyColumn(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.fillMaxHeight(4 / 5f)) {
|
||||
modifier = Modifier.fillMaxHeight(4 / 5f),
|
||||
) {
|
||||
items(sortedPackages, key = { it.packageName }) { pack ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxSize().padding(5.dp)) {
|
||||
modifier = Modifier.fillMaxSize().padding(5.dp),
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth(fillMaxWidth)) {
|
||||
val drawable =
|
||||
pack.applicationInfo?.loadIcon(context.packageManager)
|
||||
|
@ -223,28 +245,34 @@ fun ConfigScreen(
|
|||
Image(
|
||||
painter = DrawablePainter(drawable),
|
||||
stringResource(id = R.string.icon),
|
||||
modifier = Modifier.size(50.dp, 50.dp))
|
||||
modifier = Modifier.size(50.dp, 50.dp),
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
Icons.Rounded.Android,
|
||||
stringResource(id = R.string.edit),
|
||||
modifier = Modifier.size(50.dp, 50.dp))
|
||||
modifier = Modifier.size(50.dp, 50.dp),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
viewModel.getPackageLabel(pack),
|
||||
modifier = Modifier.padding(5.dp))
|
||||
modifier = Modifier.padding(5.dp),
|
||||
)
|
||||
}
|
||||
Checkbox(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
checked =
|
||||
(uiState.checkedPackageNames.contains(pack.packageName)),
|
||||
(uiState.checkedPackageNames.contains(
|
||||
pack.packageName
|
||||
)),
|
||||
onCheckedChange = {
|
||||
if (it) {
|
||||
viewModel.onAddCheckedPackage(pack.packageName)
|
||||
} else {
|
||||
viewModel.onRemoveCheckedPackage(pack.packageName)
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -252,7 +280,8 @@ fun ConfigScreen(
|
|||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
||||
horizontalArrangement = Arrangement.Center) {
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
TextButton(onClick = { showApplicationsDialog = false }) {
|
||||
Text(stringResource(R.string.done))
|
||||
}
|
||||
|
@ -287,19 +316,23 @@ fun ConfigScreen(
|
|||
}
|
||||
},
|
||||
containerColor = fobColor,
|
||||
shape = RoundedCornerShape(16.dp)) {
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Save,
|
||||
contentDescription = stringResource(id = R.string.save_changes),
|
||||
tint = Color.DarkGray)
|
||||
tint = Color.DarkGray,
|
||||
)
|
||||
}
|
||||
}) {
|
||||
},
|
||||
) {
|
||||
Column {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier =
|
||||
Modifier.verticalScroll(rememberScrollState()).weight(1f, true).fillMaxSize()) {
|
||||
Modifier.verticalScroll(rememberScrollState()).weight(1f, true).fillMaxSize(),
|
||||
) {
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
shadowElevation = 2.dp,
|
||||
|
@ -311,57 +344,55 @@ fun ConfigScreen(
|
|||
} else {
|
||||
Modifier.fillMaxWidth(fillMaxWidth)
|
||||
})
|
||||
.padding(top = 50.dp, bottom = 10.dp)) {
|
||||
.padding(top = 50.dp, bottom = 10.dp),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.padding(15.dp).focusGroup()) {
|
||||
modifier = Modifier.padding(15.dp).focusGroup(),
|
||||
) {
|
||||
SectionTitle(
|
||||
stringResource(R.string.interface_), padding = screenPadding)
|
||||
stringResource(R.string.interface_),
|
||||
padding = screenPadding,
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.tunnelName,
|
||||
onValueChange = { value -> viewModel.onTunnelNameChange(value) },
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.name),
|
||||
hint = stringResource(R.string.tunnel_name).lowercase(),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester))
|
||||
modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
|
||||
)
|
||||
OutlinedTextField(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth().clickable {
|
||||
showAuthPrompt = true
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth().clickable { showAuthPrompt = true },
|
||||
value = uiState.interfaceProxy.privateKey,
|
||||
visualTransformation =
|
||||
if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) ||
|
||||
isAuthenticated)
|
||||
if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated)
|
||||
VisualTransformation.None
|
||||
else PasswordVisualTransformation(),
|
||||
enabled =
|
||||
(id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
|
||||
enabled = (id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
|
||||
onValueChange = { value -> viewModel.onPrivateKeyChange(value) },
|
||||
trailingIcon = {
|
||||
IconButton(
|
||||
modifier = Modifier.focusRequester(FocusRequester.Default),
|
||||
onClick = { viewModel.generateKeyPair() }) {
|
||||
onClick = { viewModel.generateKeyPair() },
|
||||
) {
|
||||
Icon(
|
||||
Icons.Rounded.Refresh,
|
||||
stringResource(R.string.rotate_keys),
|
||||
tint = Color.White)
|
||||
tint = Color.White,
|
||||
)
|
||||
}
|
||||
},
|
||||
label = { Text(stringResource(R.string.private_key)) },
|
||||
singleLine = true,
|
||||
placeholder = { Text(stringResource(R.string.base64_key)) },
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions)
|
||||
keyboardActions = keyboardActions,
|
||||
)
|
||||
OutlinedTextField(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(FocusRequester.Default),
|
||||
Modifier.fillMaxWidth().focusRequester(FocusRequester.Default),
|
||||
value = uiState.interfaceProxy.publicKey,
|
||||
enabled = false,
|
||||
onValueChange = {},
|
||||
|
@ -370,67 +401,64 @@ fun ConfigScreen(
|
|||
modifier = Modifier.focusRequester(FocusRequester.Default),
|
||||
onClick = {
|
||||
clipboardManager.setText(
|
||||
AnnotatedString(uiState.interfaceProxy.publicKey))
|
||||
}) {
|
||||
AnnotatedString(uiState.interfaceProxy.publicKey),
|
||||
)
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
Icons.Rounded.ContentCopy,
|
||||
stringResource(R.string.copy_public_key),
|
||||
tint = Color.White)
|
||||
tint = Color.White,
|
||||
)
|
||||
}
|
||||
},
|
||||
label = { Text(stringResource(R.string.public_key)) },
|
||||
singleLine = true,
|
||||
placeholder = { Text(stringResource(R.string.base64_key)) },
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions)
|
||||
keyboardActions = keyboardActions,
|
||||
)
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.addresses,
|
||||
onValueChange = { value ->
|
||||
viewModel.onAddressesChanged(value)
|
||||
},
|
||||
onValueChange = { value -> viewModel.onAddressesChanged(value) },
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.addresses),
|
||||
hint = stringResource(R.string.comma_separated_list),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth(3 / 5f)
|
||||
.padding(end = 5.dp))
|
||||
modifier = Modifier.fillMaxWidth(3 / 5f).padding(end = 5.dp),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.listenPort,
|
||||
onValueChange = { value ->
|
||||
viewModel.onListenPortChanged(value)
|
||||
},
|
||||
onValueChange = { value -> viewModel.onListenPortChanged(value) },
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.listen_port),
|
||||
hint = stringResource(R.string.random),
|
||||
modifier = Modifier.width(IntrinsicSize.Min))
|
||||
modifier = Modifier.width(IntrinsicSize.Min),
|
||||
)
|
||||
}
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.dnsServers,
|
||||
onValueChange = { value ->
|
||||
viewModel.onDnsServersChanged(value)
|
||||
},
|
||||
onValueChange = { value -> viewModel.onDnsServersChanged(value) },
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.dns_servers),
|
||||
hint = stringResource(R.string.comma_separated_list),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth(3 / 5f)
|
||||
.padding(end = 5.dp))
|
||||
modifier = Modifier.fillMaxWidth(3 / 5f).padding(end = 5.dp),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.mtu,
|
||||
onValueChange = { value -> viewModel.onMtuChanged(value) },
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.mtu),
|
||||
hint = stringResource(R.string.auto),
|
||||
modifier = Modifier.width(IntrinsicSize.Min))
|
||||
modifier = Modifier.width(IntrinsicSize.Min),
|
||||
)
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
||||
horizontalArrangement = Arrangement.Center) {
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
TextButton(onClick = { showApplicationsDialog = true }) {
|
||||
Text(applicationButtonText())
|
||||
}
|
||||
|
@ -449,18 +477,22 @@ fun ConfigScreen(
|
|||
} else {
|
||||
Modifier.fillMaxWidth(fillMaxWidth)
|
||||
})
|
||||
.padding(top = 10.dp, bottom = 10.dp)) {
|
||||
.padding(top = 10.dp, bottom = 10.dp),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier =
|
||||
Modifier.padding(horizontal = 15.dp).padding(bottom = 10.dp)) {
|
||||
modifier = Modifier.padding(horizontal = 15.dp).padding(bottom = 10.dp),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 5.dp)) {
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 5.dp),
|
||||
) {
|
||||
SectionTitle(
|
||||
stringResource(R.string.peer), padding = screenPadding)
|
||||
stringResource(R.string.peer),
|
||||
padding = screenPadding,
|
||||
)
|
||||
IconButton(onClick = { viewModel.onDeletePeer(index) }) {
|
||||
Icon(Icons.Rounded.Delete, stringResource(R.string.delete))
|
||||
}
|
||||
|
@ -474,7 +506,8 @@ fun ConfigScreen(
|
|||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.public_key),
|
||||
hint = stringResource(R.string.base64_key),
|
||||
modifier = Modifier.fillMaxWidth())
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = peer.preSharedKey,
|
||||
onValueChange = { value ->
|
||||
|
@ -483,7 +516,8 @@ fun ConfigScreen(
|
|||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.preshared_key),
|
||||
hint = stringResource(R.string.optional),
|
||||
modifier = Modifier.fillMaxWidth())
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = peer.persistentKeepalive,
|
||||
|
@ -494,7 +528,8 @@ fun ConfigScreen(
|
|||
trailingIcon = {
|
||||
Text(
|
||||
stringResource(R.string.seconds),
|
||||
modifier = Modifier.padding(end = 10.dp))
|
||||
modifier = Modifier.padding(end = 10.dp),
|
||||
)
|
||||
},
|
||||
label = { Text(stringResource(R.string.persistent_keepalive)) },
|
||||
singleLine = true,
|
||||
|
@ -502,7 +537,8 @@ fun ConfigScreen(
|
|||
Text(stringResource(R.string.optional_no_recommend))
|
||||
},
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions)
|
||||
keyboardActions = keyboardActions,
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = peer.endpoint,
|
||||
onValueChange = { value ->
|
||||
|
@ -511,7 +547,8 @@ fun ConfigScreen(
|
|||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.endpoint),
|
||||
hint = stringResource(R.string.endpoint).lowercase(),
|
||||
modifier = Modifier.fillMaxWidth())
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = peer.allowedIps,
|
||||
|
@ -525,17 +562,20 @@ fun ConfigScreen(
|
|||
Text(stringResource(R.string.comma_separated_list))
|
||||
},
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions)
|
||||
keyboardActions = keyboardActions,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxSize().padding(bottom = 140.dp)) {
|
||||
modifier = Modifier.fillMaxSize().padding(bottom = 140.dp),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center) {
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
TextButton(onClick = { viewModel.addEmptyPeer() }) {
|
||||
Text(stringResource(R.string.add_peer))
|
||||
}
|
||||
|
|
|
@ -46,9 +46,11 @@ constructor(
|
|||
private val _uiState = MutableStateFlow(ConfigUiState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
fun init(tunnelId : String) = viewModelScope.launch(Dispatchers.IO) {
|
||||
fun init(tunnelId: String) =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val packages = getQueriedPackages("")
|
||||
val state = if(tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
||||
val state =
|
||||
if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
||||
val tunnelConfig =
|
||||
tunnelConfigRepository.getAll().firstOrNull { it.id.toString() == tunnelId }
|
||||
if (tunnelConfig != null) {
|
||||
|
@ -76,7 +78,8 @@ constructor(
|
|||
isAllApplicationsEnabled,
|
||||
false,
|
||||
tunnelConfig,
|
||||
tunnelConfig.name)
|
||||
tunnelConfig.name,
|
||||
)
|
||||
} else {
|
||||
ConfigUiState(loading = false, packages = packages)
|
||||
}
|
||||
|
@ -85,6 +88,7 @@ constructor(
|
|||
}
|
||||
_uiState.value = state
|
||||
}
|
||||
|
||||
fun onTunnelNameChange(name: String) {
|
||||
_uiState.value = _uiState.value.copy(tunnelName = name)
|
||||
}
|
||||
|
@ -95,7 +99,9 @@ constructor(
|
|||
|
||||
fun onAddCheckedPackage(packageName: String) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(checkedPackageNames = _uiState.value.checkedPackageNames + packageName)
|
||||
_uiState.value.copy(
|
||||
checkedPackageNames = _uiState.value.checkedPackageNames + packageName
|
||||
)
|
||||
}
|
||||
|
||||
fun onAllApplicationsChange(isAllApplicationsEnabled: Boolean) {
|
||||
|
@ -104,7 +110,9 @@ constructor(
|
|||
|
||||
fun onRemoveCheckedPackage(packageName: String) {
|
||||
_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> {
|
||||
|
@ -124,7 +132,9 @@ constructor(
|
|||
private fun getPackagesHoldingPermissions(permissions: Array<String>): List<PackageInfo> {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
packageManager.getPackagesHoldingPermissions(
|
||||
permissions, PackageManager.PackageInfoFlags.of(0L))
|
||||
permissions,
|
||||
PackageManager.PackageInfoFlags.of(0L),
|
||||
)
|
||||
} else {
|
||||
packageManager.getPackagesHoldingPermissions(permissions, 0)
|
||||
}
|
||||
|
@ -135,9 +145,7 @@ constructor(
|
|||
}
|
||||
|
||||
private fun saveConfig(tunnelConfig: TunnelConfig) =
|
||||
viewModelScope.launch {
|
||||
tunnelConfigRepository.save(tunnelConfig)
|
||||
}
|
||||
viewModelScope.launch { tunnelConfigRepository.save(tunnelConfig) }
|
||||
|
||||
private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) =
|
||||
viewModelScope.launch {
|
||||
|
@ -179,7 +187,9 @@ constructor(
|
|||
val builder = Interface.Builder()
|
||||
builder.parsePrivateKey(_uiState.value.interfaceProxy.privateKey.trim())
|
||||
builder.parseAddresses(_uiState.value.interfaceProxy.addresses.trim())
|
||||
if (_uiState.value.interfaceProxy.dnsServers.isNotEmpty()) {
|
||||
builder.parseDnsServers(_uiState.value.interfaceProxy.dnsServers.trim())
|
||||
}
|
||||
if (_uiState.value.interfaceProxy.mtu.isNotEmpty())
|
||||
builder.parseMtu(_uiState.value.interfaceProxy.mtu.trim())
|
||||
if (_uiState.value.interfaceProxy.listenPort.isNotEmpty()) {
|
||||
|
@ -198,7 +208,9 @@ constructor(
|
|||
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
||||
val tunnelConfig =
|
||||
_uiState.value.tunnel?.copy(
|
||||
name = _uiState.value.tunnelName, wgQuick = config.toWgQuickString())
|
||||
name = _uiState.value.tunnelName,
|
||||
wgQuick = config.toWgQuickString(),
|
||||
)
|
||||
updateTunnelConfig(tunnelConfig)
|
||||
Result.Success(Event.Message.ConfigSaved)
|
||||
} catch (e: Exception) {
|
||||
|
@ -211,7 +223,10 @@ constructor(
|
|||
_uiState.value.copy(
|
||||
proxyPeers =
|
||||
_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) {
|
||||
|
@ -219,7 +234,10 @@ constructor(
|
|||
_uiState.value.copy(
|
||||
proxyPeers =
|
||||
_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) {
|
||||
|
@ -227,7 +245,10 @@ constructor(
|
|||
_uiState.value.copy(
|
||||
proxyPeers =
|
||||
_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) {
|
||||
|
@ -235,7 +256,10 @@ constructor(
|
|||
_uiState.value.copy(
|
||||
proxyPeers =
|
||||
_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) {
|
||||
|
@ -243,12 +267,16 @@ constructor(
|
|||
_uiState.value.copy(
|
||||
proxyPeers =
|
||||
_uiState.value.proxyPeers.update(
|
||||
index, _uiState.value.proxyPeers[index].copy(persistentKeepalive = value)))
|
||||
index,
|
||||
_uiState.value.proxyPeers[index].copy(persistentKeepalive = value),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onDeletePeer(index: Int) {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
proxyPeers = _uiState.value.proxyPeers.removeAt(index)
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
proxyPeers = _uiState.value.proxyPeers.removeAt(index),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -263,22 +291,30 @@ constructor(
|
|||
interfaceProxy =
|
||||
_uiState.value.interfaceProxy.copy(
|
||||
privateKey = keyPair.privateKey.toBase64(),
|
||||
publicKey = keyPair.publicKey.toBase64()))
|
||||
publicKey = keyPair.publicKey.toBase64(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onAddressesChanged(value: String) {
|
||||
_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) {
|
||||
_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) {
|
||||
_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) {
|
||||
|
@ -288,12 +324,16 @@ constructor(
|
|||
|
||||
private fun onInterfacePublicKeyChange(value: String) {
|
||||
_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) {
|
||||
_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)) {
|
||||
val pair = KeyPair(Key.fromBase64(value))
|
||||
onInterfacePublicKeyChange(pair.publicKey.toBase64())
|
||||
|
|
|
@ -107,6 +107,7 @@ import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
|
@ -127,6 +128,7 @@ fun MainScreen(
|
|||
var showBottomSheet by remember { mutableStateOf(false) }
|
||||
|
||||
var showPrimaryChangeAlertDialog by remember { mutableStateOf(false) }
|
||||
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
|
||||
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
|
@ -155,21 +157,28 @@ fun MainScreen(
|
|||
context.packageManager.queryIntentActivities(
|
||||
intent,
|
||||
PackageManager.ResolveInfoFlags.of(
|
||||
PackageManager.MATCH_DEFAULT_ONLY.toLong()))
|
||||
PackageManager.MATCH_DEFAULT_ONLY.toLong(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
context.packageManager.queryIntentActivities(
|
||||
intent, PackageManager.MATCH_DEFAULT_ONLY)
|
||||
intent,
|
||||
PackageManager.MATCH_DEFAULT_ONLY,
|
||||
)
|
||||
}
|
||||
if (activitiesToResolveIntent.all {
|
||||
if (
|
||||
activitiesToResolveIntent.all {
|
||||
val name = it.activityInfo.packageName
|
||||
name.startsWith(Constants.GOOGLE_TV_EXPLORER_STUB) ||
|
||||
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
|
||||
}) {
|
||||
}
|
||||
) {
|
||||
showSnackbarMessage(Event.Error.FileExplorerRequired.message)
|
||||
}
|
||||
return intent
|
||||
}
|
||||
}) { data ->
|
||||
},
|
||||
) { data ->
|
||||
if (data == null) return@rememberLauncherForActivityResult
|
||||
scope.launch {
|
||||
viewModel.onTunnelFileSelected(data).let {
|
||||
|
@ -194,7 +203,8 @@ fun MainScreen(
|
|||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
AnimatedVisibility(showPrimaryChangeAlertDialog) {
|
||||
AlertDialog(
|
||||
|
@ -205,7 +215,8 @@ fun MainScreen(
|
|||
viewModel.onDefaultTunnelChange(selectedTunnel)
|
||||
showPrimaryChangeAlertDialog = false
|
||||
selectedTunnel = null
|
||||
}) {
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(R.string.okay))
|
||||
}
|
||||
},
|
||||
|
@ -215,7 +226,32 @@ fun MainScreen(
|
|||
}
|
||||
},
|
||||
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) {
|
||||
|
@ -228,7 +264,8 @@ fun MainScreen(
|
|||
detectTapGestures(
|
||||
onTap = {
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) selectedTunnel = null
|
||||
})
|
||||
},
|
||||
)
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.End,
|
||||
topBar = {
|
||||
|
@ -238,25 +275,37 @@ fun MainScreen(
|
|||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
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 {
|
||||
Icon(
|
||||
Icons.Rounded.Bolt,
|
||||
stringResource(id = R.string.auto),
|
||||
modifier = Modifier.size(25.dp),
|
||||
tint = if(uiState.settings.isAutoTunnelPaused) Color.Gray else mint)
|
||||
tint =
|
||||
if (uiState.settings.isAutoTunnelPaused) Color.Gray
|
||||
else mint,
|
||||
)
|
||||
Text(
|
||||
"Auto-tunneling: ${if (uiState.settings.isAutoTunnelPaused) "paused" else "active"}",
|
||||
style = typography.bodyLarge,
|
||||
modifier = Modifier.padding(start = 10.dp))
|
||||
modifier = Modifier.padding(start = 10.dp),
|
||||
)
|
||||
}
|
||||
if(uiState.settings.isAutoTunnelPaused) TextButton(
|
||||
if (uiState.settings.isAutoTunnelPaused)
|
||||
TextButton(
|
||||
onClick = { viewModel.resumeAutoTunneling() },
|
||||
modifier = Modifier.padding(end = 10.dp)) {
|
||||
modifier = Modifier.padding(end = 10.dp),
|
||||
) {
|
||||
Text("Resume")
|
||||
} else TextButton(
|
||||
}
|
||||
else
|
||||
TextButton(
|
||||
onClick = { viewModel.pauseAutoTunneling() },
|
||||
modifier = Modifier.padding(end = 10.dp)) {
|
||||
modifier = Modifier.padding(end = 10.dp),
|
||||
) {
|
||||
Text("Pause")
|
||||
}
|
||||
}
|
||||
|
@ -267,14 +316,17 @@ fun MainScreen(
|
|||
AnimatedVisibility(
|
||||
visible = isVisible.value,
|
||||
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
||||
exit = slideOutVertically(targetOffsetY = { it * 2 })) {
|
||||
exit = slideOutVertically(targetOffsetY = { it * 2 }),
|
||||
) {
|
||||
val secondaryColor = MaterialTheme.colorScheme.secondary
|
||||
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||
var fobColor by remember { mutableStateOf(secondaryColor) }
|
||||
FloatingActionButton(
|
||||
modifier =
|
||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
||||
uiState.tunnels.isEmpty())
|
||||
(if (
|
||||
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
||||
uiState.tunnels.isEmpty()
|
||||
)
|
||||
Modifier.focusRequester(focusRequester)
|
||||
else Modifier)
|
||||
.padding(bottom = 90.dp)
|
||||
|
@ -285,25 +337,31 @@ fun MainScreen(
|
|||
},
|
||||
onClick = { showBottomSheet = true },
|
||||
containerColor = fobColor,
|
||||
shape = RoundedCornerShape(16.dp)) {
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Add,
|
||||
contentDescription = stringResource(id = R.string.add_tunnel),
|
||||
tint = Color.DarkGray)
|
||||
tint = Color.DarkGray,
|
||||
)
|
||||
}
|
||||
}
|
||||
}) { innerPadding ->
|
||||
},
|
||||
) { innerPadding ->
|
||||
AnimatedVisibility(uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxSize().padding(padding)) {
|
||||
modifier = Modifier.fillMaxSize().padding(padding),
|
||||
) {
|
||||
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
|
||||
}
|
||||
}
|
||||
if (showBottomSheet) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { showBottomSheet = false }, sheetState = sheetState) {
|
||||
onDismissRequest = { showBottomSheet = false },
|
||||
sheetState = sheetState,
|
||||
) {
|
||||
// Sheet content
|
||||
Row(
|
||||
modifier =
|
||||
|
@ -312,14 +370,17 @@ fun MainScreen(
|
|||
showBottomSheet = false
|
||||
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
||||
}
|
||||
.padding(10.dp)) {
|
||||
.padding(10.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.FileOpen,
|
||||
contentDescription = stringResource(id = R.string.open_file),
|
||||
modifier = Modifier.padding(10.dp))
|
||||
modifier = Modifier.padding(10.dp),
|
||||
)
|
||||
Text(
|
||||
stringResource(id = R.string.add_tunnels_text),
|
||||
modifier = Modifier.padding(10.dp))
|
||||
modifier = Modifier.padding(10.dp),
|
||||
)
|
||||
}
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Divider()
|
||||
|
@ -332,20 +393,26 @@ fun MainScreen(
|
|||
val scanOptions = ScanOptions()
|
||||
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
||||
scanOptions.setOrientationLocked(true)
|
||||
scanOptions.setPrompt(context.getString(R.string.scanning_qr))
|
||||
scanOptions.setPrompt(
|
||||
context.getString(R.string.scanning_qr)
|
||||
)
|
||||
scanOptions.setBeepEnabled(false)
|
||||
scanOptions.captureActivity = CaptureActivityPortrait::class.java
|
||||
scanOptions.captureActivity =
|
||||
CaptureActivityPortrait::class.java
|
||||
scanLauncher.launch(scanOptions)
|
||||
}
|
||||
}
|
||||
.padding(10.dp)) {
|
||||
.padding(10.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.QrCode,
|
||||
contentDescription = stringResource(id = R.string.qr_scan),
|
||||
modifier = Modifier.padding(10.dp))
|
||||
modifier = Modifier.padding(10.dp),
|
||||
)
|
||||
Text(
|
||||
stringResource(id = R.string.add_from_qr),
|
||||
modifier = Modifier.padding(10.dp))
|
||||
modifier = Modifier.padding(10.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
|
@ -355,16 +422,20 @@ fun MainScreen(
|
|||
.clickable {
|
||||
showBottomSheet = false
|
||||
navController.navigate(
|
||||
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}")
|
||||
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}",
|
||||
)
|
||||
}
|
||||
.padding(10.dp)) {
|
||||
.padding(10.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Create,
|
||||
contentDescription = stringResource(id = R.string.create_import),
|
||||
modifier = Modifier.padding(10.dp))
|
||||
modifier = Modifier.padding(10.dp),
|
||||
)
|
||||
Text(
|
||||
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,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
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()),
|
||||
userScrollEnabled = true,
|
||||
reverseLayout = true,
|
||||
flingBehavior = ScrollableDefaults.flingBehavior()) {
|
||||
items(uiState.tunnels,
|
||||
key = { tunnel -> tunnel.id }) { tunnel ->
|
||||
flingBehavior = ScrollableDefaults.flingBehavior(),
|
||||
) {
|
||||
items(
|
||||
uiState.tunnels,
|
||||
key = { tunnel -> tunnel.id },
|
||||
) { tunnel ->
|
||||
val leadingIconColor =
|
||||
(if (uiState.vpnState.name == tunnel.name &&
|
||||
uiState.vpnState.status == Tunnel.State.UP) {
|
||||
(if (
|
||||
uiState.vpnState.name == tunnel.name &&
|
||||
uiState.vpnState.status == Tunnel.State.UP
|
||||
) {
|
||||
uiState.vpnState.statistics
|
||||
?.mapPeerStats()
|
||||
?.map { it.value?.handshakeStatus() }
|
||||
|
@ -408,19 +487,23 @@ fun MainScreen(
|
|||
Icons.Rounded.Star,
|
||||
stringResource(R.string.status),
|
||||
tint = leadingIconColor,
|
||||
modifier = Modifier.padding(end = 10.dp).size(20.dp))
|
||||
modifier = Modifier.padding(end = 10.dp).size(20.dp),
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
Icons.Rounded.Circle,
|
||||
stringResource(R.string.status),
|
||||
tint = leadingIconColor,
|
||||
modifier = Modifier.padding(end = 15.dp).size(15.dp))
|
||||
modifier = Modifier.padding(end = 15.dp).size(15.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
text = tunnel.name,
|
||||
onHold = {
|
||||
if ((uiState.vpnState.status == Tunnel.State.UP) &&
|
||||
(tunnel.name == uiState.vpnState.name)) {
|
||||
if (
|
||||
(uiState.vpnState.status == Tunnel.State.UP) &&
|
||||
(tunnel.name == uiState.vpnState.name)
|
||||
) {
|
||||
showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
||||
return@RowListItem
|
||||
}
|
||||
|
@ -429,8 +512,10 @@ fun MainScreen(
|
|||
},
|
||||
onClick = {
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
if (uiState.vpnState.status == Tunnel.State.UP &&
|
||||
(uiState.vpnState.name == tunnel.name)) {
|
||||
if (
|
||||
uiState.vpnState.status == Tunnel.State.UP &&
|
||||
(uiState.vpnState.name == tunnel.name)
|
||||
) {
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
} else {
|
||||
|
@ -441,38 +526,56 @@ fun MainScreen(
|
|||
statistics = uiState.vpnState.statistics,
|
||||
expanded = expanded.value,
|
||||
rowButton = {
|
||||
if (tunnel.id == selectedTunnel?.id &&
|
||||
!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
if (
|
||||
tunnel.id == selectedTunnel?.id &&
|
||||
!WireGuardAutoTunnel.isRunningOnAndroidTv()
|
||||
) {
|
||||
Row {
|
||||
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (uiState.settings.isAutoTunnelEnabled && !uiState.settings.isAutoTunnelPaused) {
|
||||
if (
|
||||
uiState.settings.isAutoTunnelEnabled &&
|
||||
!uiState.settings.isAutoTunnelPaused
|
||||
) {
|
||||
showSnackbarMessage(
|
||||
Event.Message.AutoTunnelOffAction.message)
|
||||
Event.Message.AutoTunnelOffAction.message,
|
||||
)
|
||||
} else {
|
||||
showPrimaryChangeAlertDialog = true
|
||||
}
|
||||
}) {
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
Icons.Rounded.Star,
|
||||
stringResource(id = R.string.set_primary))
|
||||
stringResource(id = R.string.set_primary),
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (uiState.settings.isAutoTunnelEnabled && uiState.settings.isTunnelConfigDefault(tunnel)
|
||||
&& !uiState.settings.isAutoTunnelPaused) {
|
||||
if (
|
||||
uiState.settings.isAutoTunnelEnabled &&
|
||||
uiState.settings.isTunnelConfigDefault(
|
||||
tunnel,
|
||||
) &&
|
||||
!uiState.settings.isAutoTunnelPaused
|
||||
) {
|
||||
showSnackbarMessage(
|
||||
Event.Message.AutoTunnelOffAction.message)
|
||||
} else navController.navigate(
|
||||
"${Screen.Config.route}/${selectedTunnel?.id}")
|
||||
}) {
|
||||
Event.Message.AutoTunnelOffAction.message,
|
||||
)
|
||||
} else
|
||||
navController.navigate(
|
||||
"${Screen.Config.route}/${selectedTunnel?.id}",
|
||||
)
|
||||
},
|
||||
) {
|
||||
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
|
||||
}
|
||||
IconButton(
|
||||
modifier = Modifier.focusable(),
|
||||
onClick = { viewModel.onDelete(tunnel) }) {
|
||||
onClick = { showDeleteTunnelAlertDialog = true },
|
||||
) {
|
||||
Icon(Icons.Rounded.Delete, stringResource(id = R.string.delete))
|
||||
}
|
||||
}
|
||||
|
@ -493,7 +596,8 @@ fun MainScreen(
|
|||
onCheckedChange = { checked ->
|
||||
if (!checked) expanded.value = false
|
||||
onTunnelToggle(checked, tunnel)
|
||||
})
|
||||
},
|
||||
)
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Row {
|
||||
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
|
||||
|
@ -501,53 +605,73 @@ fun MainScreen(
|
|||
onClick = {
|
||||
if (uiState.settings.isAutoTunnelEnabled) {
|
||||
showSnackbarMessage(
|
||||
Event.Message.AutoTunnelOffAction.message)
|
||||
Event.Message.AutoTunnelOffAction.message,
|
||||
)
|
||||
} else {
|
||||
selectedTunnel = tunnel
|
||||
showPrimaryChangeAlertDialog = true
|
||||
}
|
||||
}) {
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
Icons.Rounded.Star,
|
||||
stringResource(id = R.string.set_primary))
|
||||
stringResource(id = R.string.set_primary),
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
onClick = {
|
||||
if (uiState.vpnState.status == Tunnel.State.UP &&
|
||||
(uiState.vpnState.name == tunnel.name)) {
|
||||
if (
|
||||
uiState.vpnState.status == Tunnel.State.UP &&
|
||||
(uiState.vpnState.name == tunnel.name)
|
||||
) {
|
||||
expanded.value = !expanded.value
|
||||
} else {
|
||||
showSnackbarMessage(Event.Message.TunnelOnAction.message)
|
||||
showSnackbarMessage(
|
||||
Event.Message.TunnelOnAction.message
|
||||
)
|
||||
}
|
||||
}) {
|
||||
},
|
||||
) {
|
||||
Icon(Icons.Rounded.Info, stringResource(R.string.info))
|
||||
}
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (uiState.vpnState.status == Tunnel.State.UP &&
|
||||
tunnel.name == uiState.vpnState.name) {
|
||||
showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
||||
if (
|
||||
uiState.vpnState.status == Tunnel.State.UP &&
|
||||
tunnel.name == uiState.vpnState.name
|
||||
) {
|
||||
showSnackbarMessage(
|
||||
Event.Message.TunnelOffAction.message
|
||||
)
|
||||
} else {
|
||||
navController.navigate(
|
||||
"${Screen.Config.route}/${tunnel.id}")
|
||||
"${Screen.Config.route}/${tunnel.id}",
|
||||
)
|
||||
}
|
||||
}) {
|
||||
},
|
||||
) {
|
||||
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
|
||||
}
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (uiState.vpnState.status == Tunnel.State.UP &&
|
||||
tunnel.name == uiState.vpnState.name) {
|
||||
showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
||||
if (
|
||||
uiState.vpnState.status == Tunnel.State.UP &&
|
||||
tunnel.name == uiState.vpnState.name
|
||||
) {
|
||||
showSnackbarMessage(
|
||||
Event.Message.TunnelOffAction.message
|
||||
)
|
||||
} else {
|
||||
viewModel.onDelete(tunnel)
|
||||
showDeleteTunnelAlertDialog = true
|
||||
}
|
||||
}) {
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
Icons.Rounded.Delete,
|
||||
stringResource(id = R.string.delete))
|
||||
stringResource(id = R.string.delete),
|
||||
)
|
||||
}
|
||||
TunnelSwitch()
|
||||
}
|
||||
|
@ -555,7 +679,8 @@ fun MainScreen(
|
|||
TunnelSwitch()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,11 +14,7 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
|||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||
import com.zaneschepke.wireguardautotunnel.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.WireGuardTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||
|
@ -32,6 +28,7 @@ import kotlinx.coroutines.flow.combine
|
|||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.InputStream
|
||||
import java.util.zip.ZipInputStream
|
||||
import javax.inject.Inject
|
||||
|
@ -58,21 +55,21 @@ constructor(
|
|||
.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
||||
MainUiState())
|
||||
MainUiState(),
|
||||
)
|
||||
|
||||
private fun validateWatcherServiceState(settings: Settings) = viewModelScope.launch(Dispatchers.IO) {
|
||||
val watcherState =
|
||||
ServiceManager.getServiceState(
|
||||
application.applicationContext, WireGuardConnectivityWatcherService::class.java)
|
||||
if (settings.isAutoTunnelEnabled &&
|
||||
watcherState == ServiceState.STOPPED) {
|
||||
private fun validateWatcherServiceState(settings: Settings) =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
ServiceManager.startWatcherService(application.applicationContext)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopWatcherService() = viewModelScope.launch(Dispatchers.IO) {
|
||||
private fun stopWatcherService() =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
ServiceManager.stopWatcherService(application.applicationContext)
|
||||
}
|
||||
|
||||
fun onDelete(tunnel: TunnelConfig) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
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()
|
||||
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())
|
||||
}
|
||||
|
||||
private fun stopActiveTunnel() =
|
||||
viewModelScope.async(Dispatchers.IO) {
|
||||
if (ServiceManager.getServiceState(
|
||||
application.applicationContext, WireGuardTunnelService::class.java) ==
|
||||
ServiceState.STARTED) {
|
||||
onTunnelStop()
|
||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||
}
|
||||
}
|
||||
|
||||
fun onTunnelStop() = viewModelScope.launch(Dispatchers.IO) {
|
||||
fun onTunnelStop() =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
Timber.d("Stopping active tunnel")
|
||||
ServiceManager.stopVpnService(application.applicationContext)
|
||||
}
|
||||
|
||||
|
@ -144,7 +143,8 @@ constructor(
|
|||
if (isValidUriContentScheme(uri)) {
|
||||
val fileName = getFileName(application.applicationContext, uri)
|
||||
when (getFileExtensionFromFileName(fileName)) {
|
||||
Constants.CONF_FILE_EXTENSION -> saveTunnelFromConfUri(fileName, uri).let {
|
||||
Constants.CONF_FILE_EXTENSION ->
|
||||
saveTunnelFromConfUri(fileName, uri).let {
|
||||
when (it) {
|
||||
is Result.Error -> return Result.Error(Event.Error.FileReadFailed)
|
||||
is Result.Success -> return it
|
||||
|
@ -166,7 +166,8 @@ constructor(
|
|||
ZipInputStream(getInputStreamFromUri(uri)).use { zip ->
|
||||
generateSequence { zip.nextEntry }
|
||||
.filterNot {
|
||||
it.isDirectory || getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
|
||||
it.isDirectory ||
|
||||
getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
|
||||
}
|
||||
.forEach {
|
||||
val name = getNameFromFileName(it.name)
|
||||
|
@ -193,11 +194,13 @@ constructor(
|
|||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||
}
|
||||
|
||||
fun pauseAutoTunneling() = viewModelScope.launch {
|
||||
fun pauseAutoTunneling() =
|
||||
viewModelScope.launch {
|
||||
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
|
||||
}
|
||||
|
||||
fun resumeAutoTunneling() = viewModelScope.launch {
|
||||
fun resumeAutoTunneling() =
|
||||
viewModelScope.launch {
|
||||
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
|
||||
}
|
||||
|
||||
|
@ -233,6 +236,7 @@ constructor(
|
|||
private fun isValidUriContentScheme(uri: Uri): Boolean {
|
||||
return uri.scheme == Constants.URI_CONTENT_SCHEME
|
||||
}
|
||||
|
||||
private fun getFileName(context: Context, uri: Uri): String {
|
||||
return getFileNameByCursor(context, uri) ?: NumberUtils.generateRandomTunnelName()
|
||||
}
|
||||
|
@ -252,9 +256,11 @@ constructor(
|
|||
private fun saveSettings(settings: Settings) =
|
||||
viewModelScope.launch(Dispatchers.IO) { settingsRepository.save(settings) }
|
||||
|
||||
fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) = viewModelScope.launch {
|
||||
fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) =
|
||||
viewModelScope.launch {
|
||||
if (selectedTunnel != null) {
|
||||
saveSettings(uiState.value.settings.copy(defaultTunnel = selectedTunnel.toString())).join()
|
||||
saveSettings(uiState.value.settings.copy(defaultTunnel = selectedTunnel.toString()))
|
||||
.join()
|
||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Context.POWER_SERVICE
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.provider.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.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
|
@ -83,7 +90,8 @@ import java.io.File
|
|||
|
||||
@OptIn(
|
||||
ExperimentalPermissionsApi::class,
|
||||
ExperimentalLayoutApi::class)
|
||||
ExperimentalLayoutApi::class,
|
||||
)
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
viewModel: SettingsViewModel = hiltViewModel(),
|
||||
|
@ -115,6 +123,16 @@ fun SettingsScreen(
|
|||
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() {
|
||||
try {
|
||||
val files = uiState.tunnels.map { File(context.cacheDir, "${it.name}.conf") }
|
||||
|
@ -129,6 +147,28 @@ 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() {
|
||||
if (currentText.isNotEmpty()) {
|
||||
viewModel.onSaveTrustedSSID(currentText).let {
|
||||
|
@ -159,7 +199,10 @@ fun SettingsScreen(
|
|||
}
|
||||
|
||||
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()
|
||||
} else {
|
||||
val backgroundLocationState =
|
||||
|
@ -185,8 +228,9 @@ fun SettingsScreen(
|
|||
TextButton(
|
||||
onClick = {
|
||||
showLocationServicesAlertDialog = false
|
||||
viewModel.toggleAutoTunnel()
|
||||
}) {
|
||||
handleAutoTunnelToggle()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(R.string.okay))
|
||||
}
|
||||
},
|
||||
|
@ -196,28 +240,33 @@ fun SettingsScreen(
|
|||
}
|
||||
},
|
||||
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) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.fillMaxSize().verticalScroll(scrollState).padding(padding)) {
|
||||
modifier = Modifier.fillMaxSize().verticalScroll(scrollState).padding(padding),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Rounded.LocationOff,
|
||||
contentDescription = stringResource(id = R.string.map),
|
||||
modifier = Modifier.padding(30.dp).size(128.dp))
|
||||
modifier = Modifier.padding(30.dp).size(128.dp),
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.prominent_background_location_title),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(30.dp),
|
||||
fontSize = 20.sp)
|
||||
fontSize = 20.sp,
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.prominent_background_location_message),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(30.dp),
|
||||
fontSize = 15.sp)
|
||||
fontSize = 15.sp,
|
||||
)
|
||||
Row(
|
||||
modifier =
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
|
@ -226,7 +275,8 @@ fun SettingsScreen(
|
|||
Modifier.fillMaxWidth().padding(30.dp)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly) {
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
TextButton(onClick = { viewModel.setLocationDisclosureShown() }) {
|
||||
Text(stringResource(id = R.string.no_thanks))
|
||||
}
|
||||
|
@ -235,7 +285,8 @@ fun SettingsScreen(
|
|||
onClick = {
|
||||
openSettings()
|
||||
viewModel.setLocationDisclosureShown()
|
||||
}) {
|
||||
},
|
||||
) {
|
||||
Text(stringResource(id = R.string.turn_on))
|
||||
}
|
||||
}
|
||||
|
@ -255,19 +306,22 @@ fun SettingsScreen(
|
|||
onFailure = {
|
||||
showAuthPrompt = false
|
||||
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (uiState.tunnels.isEmpty() && uiState.isLocationDisclosureShown) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxSize().padding(padding)) {
|
||||
modifier = Modifier.fillMaxSize().padding(padding),
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.one_tunnel_required),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(15.dp),
|
||||
fontStyle = FontStyle.Italic)
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (uiState.isLocationDisclosureShown && uiState.tunnels.isNotEmpty()) {
|
||||
|
@ -276,9 +330,12 @@ fun SettingsScreen(
|
|||
verticalArrangement = Arrangement.Top,
|
||||
modifier =
|
||||
Modifier.fillMaxSize().verticalScroll(scrollState).clickable(
|
||||
indication = null, interactionSource = interactionSource) {
|
||||
indication = null,
|
||||
interactionSource = interactionSource,
|
||||
) {
|
||||
focusManager.clearFocus()
|
||||
}) {
|
||||
},
|
||||
) {
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
shadowElevation = 2.dp,
|
||||
|
@ -292,14 +349,17 @@ fun SettingsScreen(
|
|||
} else {
|
||||
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 60.dp)
|
||||
})
|
||||
.padding(bottom = 10.dp)) {
|
||||
.padding(bottom = 10.dp),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.padding(15.dp)) {
|
||||
modifier = Modifier.padding(15.dp),
|
||||
) {
|
||||
SectionTitle(
|
||||
title = stringResource(id = R.string.auto_tunneling),
|
||||
padding = screenPadding)
|
||||
padding = screenPadding,
|
||||
)
|
||||
ConfigurationToggle(
|
||||
stringResource(id = R.string.tunnel_on_wifi),
|
||||
enabled =
|
||||
|
@ -308,30 +368,41 @@ fun SettingsScreen(
|
|||
checked = uiState.settings.isTunnelOnWifiEnabled,
|
||||
padding = screenPadding,
|
||||
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) {
|
||||
Column {
|
||||
FlowRow(
|
||||
modifier = Modifier.padding(screenPadding).fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp)) {
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||
) {
|
||||
uiState.settings.trustedNetworkSSIDs.forEach { ssid ->
|
||||
ClickableIconButton(
|
||||
onClick = { if(WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
onClick = {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
viewModel.onDeleteTrustedSSID(ssid)
|
||||
focusRequester2.requestFocus()
|
||||
}},
|
||||
}
|
||||
},
|
||||
onIconClick = { viewModel.onDeleteTrustedSSID(ssid) },
|
||||
text = ssid,
|
||||
icon = Icons.Filled.Close,
|
||||
enabled =
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled))
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
)
|
||||
}
|
||||
if (uiState.settings.trustedNetworkSSIDs.isEmpty()) {
|
||||
Text(
|
||||
stringResource(R.string.none),
|
||||
fontStyle = FontStyle.Italic,
|
||||
color = Color.Gray)
|
||||
color = Color.Gray,
|
||||
)
|
||||
}
|
||||
}
|
||||
OutlinedTextField(
|
||||
|
@ -343,14 +414,17 @@ fun SettingsScreen(
|
|||
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
||||
modifier =
|
||||
Modifier.padding(
|
||||
start = screenPadding, top = 5.dp, bottom = 10.dp)
|
||||
.focusRequester(focusRequester2)
|
||||
,
|
||||
start = screenPadding,
|
||||
top = 5.dp,
|
||||
bottom = 10.dp,
|
||||
)
|
||||
.focusRequester(focusRequester2),
|
||||
maxLines = 1,
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
imeAction = ImeAction.Done),
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
|
||||
trailingIcon = {
|
||||
if (currentText != "") {
|
||||
|
@ -360,15 +434,23 @@ fun SettingsScreen(
|
|||
contentDescription =
|
||||
if (currentText == "") {
|
||||
stringResource(
|
||||
id = R.string.trusted_ssid_empty_description)
|
||||
id =
|
||||
R.string
|
||||
.trusted_ssid_empty_description,
|
||||
)
|
||||
} else {
|
||||
stringResource(
|
||||
id = R.string.trusted_ssid_value_description)
|
||||
id =
|
||||
R.string
|
||||
.trusted_ssid_value_description,
|
||||
)
|
||||
},
|
||||
tint = MaterialTheme.colorScheme.primary)
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
ConfigurationToggle(
|
||||
|
@ -378,7 +460,8 @@ fun SettingsScreen(
|
|||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
checked = uiState.settings.isTunnelOnMobileDataEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = { viewModel.onToggleTunnelOnMobileData() })
|
||||
onCheckChanged = { viewModel.onToggleTunnelOnMobileData() },
|
||||
)
|
||||
ConfigurationToggle(
|
||||
stringResource(id = R.string.tunnel_on_ethernet),
|
||||
enabled =
|
||||
|
@ -386,7 +469,8 @@ fun SettingsScreen(
|
|||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
checked = uiState.settings.isTunnelOnEthernetEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = { viewModel.onToggleTunnelOnEthernet() })
|
||||
onCheckChanged = { viewModel.onToggleTunnelOnEthernet() },
|
||||
)
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.battery_saver),
|
||||
enabled =
|
||||
|
@ -394,31 +478,47 @@ fun SettingsScreen(
|
|||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
checked = uiState.settings.isBatterySaverEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = { viewModel.onToggleBatterySaver() })
|
||||
onCheckChanged = { viewModel.onToggleBatterySaver() },
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = (if(!uiState.settings.isAutoTunnelEnabled) Modifier else Modifier.focusRequester(focusRequester))
|
||||
.fillMaxSize().padding(top = 5.dp),
|
||||
horizontalArrangement = Arrangement.Center) {
|
||||
modifier =
|
||||
(if (!uiState.settings.isAutoTunnelEnabled) Modifier
|
||||
else
|
||||
Modifier.focusRequester(
|
||||
focusRequester,
|
||||
))
|
||||
.fillMaxSize()
|
||||
.padding(top = 5.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
TextButton(
|
||||
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
|
||||
onClick = {
|
||||
if (uiState.settings.isTunnelOnWifiEnabled && !uiState.settings.isAutoTunnelEnabled) {
|
||||
if (
|
||||
uiState.settings.isTunnelOnWifiEnabled &&
|
||||
!uiState.settings.isAutoTunnelEnabled
|
||||
) {
|
||||
when (false) {
|
||||
isBackgroundLocationGranted ->
|
||||
showSnackbarMessage(Event.Error.BackgroundLocationRequired.message)
|
||||
showSnackbarMessage(
|
||||
Event.Error.BackgroundLocationRequired.message
|
||||
)
|
||||
fineLocationState.status.isGranted ->
|
||||
showSnackbarMessage(Event.Error.PreciseLocationRequired.message)
|
||||
showSnackbarMessage(
|
||||
Event.Error.PreciseLocationRequired.message
|
||||
)
|
||||
viewModel.isLocationEnabled(context) ->
|
||||
showLocationServicesAlertDialog = true
|
||||
else -> {
|
||||
viewModel.toggleAutoTunnel()
|
||||
handleAutoTunnelToggle()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
viewModel.toggleAutoTunnel()
|
||||
handleAutoTunnelToggle()
|
||||
}
|
||||
}) {
|
||||
},
|
||||
) {
|
||||
val autoTunnelButtonText =
|
||||
if (uiState.settings.isAutoTunnelEnabled) {
|
||||
stringResource(R.string.disable_auto_tunnel)
|
||||
|
@ -436,13 +536,17 @@ fun SettingsScreen(
|
|||
shadowElevation = 2.dp,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier.fillMaxWidth(fillMaxWidth).padding(vertical = 10.dp)) {
|
||||
modifier = Modifier.fillMaxWidth(fillMaxWidth).padding(vertical = 10.dp),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.padding(15.dp)) {
|
||||
modifier = Modifier.padding(15.dp),
|
||||
) {
|
||||
SectionTitle(
|
||||
title = stringResource(id = R.string.kernel), padding = screenPadding)
|
||||
title = stringResource(id = R.string.kernel),
|
||||
padding = screenPadding,
|
||||
)
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.use_kernel),
|
||||
enabled =
|
||||
|
@ -451,12 +555,15 @@ fun SettingsScreen(
|
|||
(uiState.vpnState.status == Tunnel.State.UP)),
|
||||
checked = uiState.settings.isKernelEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = { viewModel.onToggleKernelMode().let {
|
||||
onCheckChanged = {
|
||||
viewModel.onToggleKernelMode().let {
|
||||
when (it) {
|
||||
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||
is Result.Success -> {}
|
||||
}
|
||||
} })
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -469,31 +576,40 @@ fun SettingsScreen(
|
|||
modifier =
|
||||
Modifier.fillMaxWidth(fillMaxWidth)
|
||||
.padding(vertical = 10.dp)
|
||||
.padding(bottom = 140.dp)) {
|
||||
.padding(bottom = 140.dp),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.padding(15.dp)) {
|
||||
modifier = Modifier.padding(15.dp),
|
||||
) {
|
||||
SectionTitle(
|
||||
title = stringResource(id = R.string.other), padding = screenPadding)
|
||||
title = stringResource(id = R.string.other),
|
||||
padding = screenPadding,
|
||||
)
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.always_on_vpn_support),
|
||||
enabled = !uiState.settings.isAutoTunnelEnabled,
|
||||
checked = uiState.settings.isAlwaysOnVpnEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = { viewModel.onToggleAlwaysOnVPN() })
|
||||
onCheckChanged = { viewModel.onToggleAlwaysOnVPN() },
|
||||
)
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.enabled_app_shortcuts),
|
||||
enabled = true,
|
||||
checked = uiState.settings.isShortcutsEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = { viewModel.onToggleShortcutsEnabled() })
|
||||
onCheckChanged = { viewModel.onToggleShortcutsEnabled() },
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
||||
horizontalArrangement = Arrangement.Center) {
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
TextButton(
|
||||
enabled = !didExportFiles, onClick = { showAuthPrompt = true }) {
|
||||
enabled = !didExportFiles,
|
||||
onClick = { showAuthPrompt = true },
|
||||
) {
|
||||
Text(stringResource(R.string.export_configs))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,5 +9,6 @@ data class SettingsUiState(
|
|||
val tunnels: List<TunnelConfig> = emptyList(),
|
||||
val vpnState: VpnState = VpnState(),
|
||||
val isLocationDisclosureShown: Boolean = true,
|
||||
val isBatteryOptimizeDisableShown: Boolean = false,
|
||||
val loading: Boolean = true
|
||||
)
|
||||
|
|
|
@ -36,16 +36,27 @@ constructor(
|
|||
private val vpnService: VpnService
|
||||
) : ViewModel() {
|
||||
|
||||
val uiState = combine(
|
||||
val uiState =
|
||||
combine(
|
||||
settingsRepository.getSettingsFlow(),
|
||||
tunnelConfigRepository.getTunnelConfigsFlow(),
|
||||
vpnService.vpnState,
|
||||
dataStoreManager.locationDisclosureFlow,
|
||||
){ settings, tunnels, tunnelState, locationDisclosure ->
|
||||
SettingsUiState(settings, tunnels, tunnelState, locationDisclosure
|
||||
?: false, false)
|
||||
}.stateIn(viewModelScope,
|
||||
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), SettingsUiState())
|
||||
dataStoreManager.preferencesFlow,
|
||||
) { settings, tunnels, tunnelState, preferences ->
|
||||
SettingsUiState(
|
||||
settings,
|
||||
tunnels,
|
||||
tunnelState,
|
||||
preferences?.get(DataStoreManager.LOCATION_DISCLOSURE_SHOWN) ?: false,
|
||||
preferences?.get(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN) ?: false,
|
||||
false
|
||||
)
|
||||
}
|
||||
.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
||||
SettingsUiState(),
|
||||
)
|
||||
|
||||
fun onSaveTrustedSSID(ssid: String): Result<Unit> {
|
||||
val trimmed = ssid.trim()
|
||||
|
@ -58,29 +69,40 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun setLocationDisclosureShown() = viewModelScope.launch {
|
||||
fun setLocationDisclosureShown() =
|
||||
viewModelScope.launch {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, true)
|
||||
}
|
||||
|
||||
fun setBatteryOptimizeDisableShown() =
|
||||
viewModelScope.launch {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, true)
|
||||
}
|
||||
|
||||
fun onToggleTunnelOnMobileData() {
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
isTunnelOnMobileDataEnabled = !uiState.value.settings.isTunnelOnMobileDataEnabled
|
||||
)
|
||||
isTunnelOnMobileDataEnabled = !uiState.value.settings.isTunnelOnMobileDataEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onDeleteTrustedSSID(ssid: String) {
|
||||
saveSettings(uiState.value.settings.copy(
|
||||
trustedNetworkSSIDs = (uiState.value.settings.trustedNetworkSSIDs - ssid).toMutableList()
|
||||
))
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
trustedNetworkSSIDs =
|
||||
(uiState.value.settings.trustedNetworkSSIDs - ssid).toMutableList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getDefaultTunnelOrFirst(): String {
|
||||
return uiState.value.settings.defaultTunnel ?: tunnelConfigRepository.getAll().first().toString()
|
||||
return uiState.value.settings.defaultTunnel
|
||||
?: tunnelConfigRepository.getAll().first().toString()
|
||||
}
|
||||
|
||||
fun toggleAutoTunnel() = viewModelScope.launch {
|
||||
fun toggleAutoTunnel() =
|
||||
viewModelScope.launch {
|
||||
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
|
||||
var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused
|
||||
|
||||
|
@ -94,28 +116,30 @@ constructor(
|
|||
uiState.value.settings.copy(
|
||||
isAutoTunnelEnabled = !isAutoTunnelEnabled,
|
||||
isAutoTunnelPaused = isAutoTunnelPaused,
|
||||
defaultTunnel = getDefaultTunnelOrFirst()
|
||||
)
|
||||
defaultTunnel = getDefaultTunnelOrFirst(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
fun onToggleAlwaysOnVPN() = viewModelScope.launch {
|
||||
val updatedSettings = uiState.value.settings.copy(
|
||||
fun onToggleAlwaysOnVPN() =
|
||||
viewModelScope.launch {
|
||||
val updatedSettings =
|
||||
uiState.value.settings.copy(
|
||||
isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled,
|
||||
defaultTunnel = getDefaultTunnelOrFirst()
|
||||
defaultTunnel = getDefaultTunnelOrFirst(),
|
||||
)
|
||||
saveSettings(updatedSettings)
|
||||
}
|
||||
|
||||
private fun saveSettings(settings: Settings) = viewModelScope.launch {
|
||||
settingsRepository.save(settings)
|
||||
}
|
||||
private fun saveSettings(settings: Settings) =
|
||||
viewModelScope.launch { settingsRepository.save(settings) }
|
||||
|
||||
fun onToggleTunnelOnEthernet() {
|
||||
saveSettings(uiState.value.settings.copy(
|
||||
isTunnelOnEthernetEnabled = !uiState.value.settings.isTunnelOnEthernetEnabled
|
||||
))
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
isTunnelOnEthernetEnabled = !uiState.value.settings.isTunnelOnEthernetEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun isLocationEnabled(context: Context): Boolean {
|
||||
|
@ -126,32 +150,32 @@ constructor(
|
|||
fun onToggleShortcutsEnabled() {
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
isShortcutsEnabled = !uiState.value.settings.isShortcutsEnabled
|
||||
)
|
||||
isShortcutsEnabled = !uiState.value.settings.isShortcutsEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onToggleBatterySaver() {
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
isBatterySaverEnabled = !uiState.value.settings.isBatterySaverEnabled
|
||||
)
|
||||
isBatterySaverEnabled = !uiState.value.settings.isBatterySaverEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveKernelMode(on: Boolean) {
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
isKernelEnabled = on
|
||||
)
|
||||
isKernelEnabled = on,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onToggleTunnelOnWifi() {
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
isTunnelOnWifiEnabled = !uiState.value.settings.isTunnelOnWifiEnabled
|
||||
)
|
||||
isTunnelOnWifiEnabled = !uiState.value.settings.isTunnelOnWifiEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -80,12 +80,16 @@ fun SupportScreen(
|
|||
fun launchEmail() {
|
||||
try {
|
||||
val intent =
|
||||
Intent(Intent.ACTION_SEND).apply {
|
||||
Intent(Intent.ACTION_SENDTO).apply {
|
||||
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))
|
||||
}
|
||||
startActivity(context, createChooser(intent, context.getString(R.string.email_chooser)), null)
|
||||
startActivity(
|
||||
context,
|
||||
createChooser(intent, context.getString(R.string.email_chooser)),
|
||||
null,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
showSnackbarMessage(Event.Error.Exception(e).message)
|
||||
}
|
||||
|
@ -103,7 +107,8 @@ fun SupportScreen(
|
|||
Modifier.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.focusable()
|
||||
.padding(padding)) {
|
||||
.padding(padding),
|
||||
) {
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
shadowElevation = 2.dp,
|
||||
|
@ -117,32 +122,38 @@ fun SupportScreen(
|
|||
} else {
|
||||
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 20.dp)
|
||||
})
|
||||
.padding(bottom = 25.dp)) {
|
||||
.padding(bottom = 25.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(20.dp)) {
|
||||
Text(
|
||||
stringResource(R.string.thank_you),
|
||||
textAlign = TextAlign.Start,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(bottom = 20.dp),
|
||||
fontSize = 16.sp)
|
||||
fontSize = 16.sp,
|
||||
)
|
||||
Text(
|
||||
stringResource(id = R.string.support_help_text),
|
||||
textAlign = TextAlign.Start,
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier.padding(bottom = 20.dp))
|
||||
modifier = Modifier.padding(bottom = 20.dp),
|
||||
)
|
||||
TextButton(
|
||||
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(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()) {
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row {
|
||||
Icon(Icons.Rounded.Book, stringResource(id = R.string.docs))
|
||||
Text(
|
||||
stringResource(id = R.string.docs_description),
|
||||
textAlign = TextAlign.Justify,
|
||||
modifier = Modifier.padding(start = 10.dp))
|
||||
modifier = Modifier.padding(start = 10.dp),
|
||||
)
|
||||
}
|
||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||
}
|
||||
|
@ -150,20 +161,24 @@ fun SupportScreen(
|
|||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||
TextButton(
|
||||
onClick = { openWebPage(context.resources.getString(R.string.discord_url)) },
|
||||
modifier = Modifier.padding(vertical = 5.dp)) {
|
||||
modifier = Modifier.padding(vertical = 5.dp),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()) {
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.discord),
|
||||
stringResource(id = R.string.discord),
|
||||
Modifier.size(25.dp))
|
||||
Modifier.size(25.dp),
|
||||
)
|
||||
Text(
|
||||
stringResource(id = R.string.discord_description),
|
||||
textAlign = TextAlign.Justify,
|
||||
modifier = Modifier.padding(start = 10.dp))
|
||||
modifier = Modifier.padding(start = 10.dp),
|
||||
)
|
||||
}
|
||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||
}
|
||||
|
@ -171,37 +186,45 @@ fun SupportScreen(
|
|||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||
TextButton(
|
||||
onClick = { openWebPage(context.resources.getString(R.string.github_url)) },
|
||||
modifier = Modifier.padding(vertical = 5.dp)) {
|
||||
modifier = Modifier.padding(vertical = 5.dp),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()) {
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.github),
|
||||
stringResource(id = R.string.github),
|
||||
Modifier.size(25.dp))
|
||||
Modifier.size(25.dp),
|
||||
)
|
||||
Text(
|
||||
"Open an issue",
|
||||
textAlign = TextAlign.Justify,
|
||||
modifier = Modifier.padding(start = 10.dp))
|
||||
modifier = Modifier.padding(start = 10.dp),
|
||||
)
|
||||
}
|
||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||
}
|
||||
}
|
||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||
TextButton(
|
||||
onClick = { launchEmail() }, modifier = Modifier.padding(vertical = 5.dp)) {
|
||||
onClick = { launchEmail() },
|
||||
modifier = Modifier.padding(vertical = 5.dp),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()) {
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row {
|
||||
Icon(Icons.Rounded.Mail, stringResource(id = R.string.email))
|
||||
Text(
|
||||
stringResource(id = R.string.email_description),
|
||||
textAlign = TextAlign.Justify,
|
||||
modifier = Modifier.padding(start = 10.dp))
|
||||
modifier = Modifier.padding(start = 10.dp),
|
||||
)
|
||||
}
|
||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||
}
|
||||
|
@ -216,11 +239,13 @@ fun SupportScreen(
|
|||
modifier =
|
||||
Modifier.clickable {
|
||||
openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
||||
})
|
||||
},
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(25.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(25.dp)) {
|
||||
modifier = Modifier.padding(25.dp),
|
||||
) {
|
||||
Text("Version: ${BuildConfig.VERSION_NAME}", modifier = Modifier.focusable())
|
||||
Text("Mode: ${if (uiState.settings.isKernelEnabled) "Kernel" else "Userspace"}")
|
||||
}
|
||||
|
|
|
@ -2,7 +2,4 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.support
|
|||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
|
||||
data class SupportUiState(
|
||||
val settings : Settings = Settings(),
|
||||
val loading : Boolean = true
|
||||
)
|
||||
data class SupportUiState(val settings: Settings = Settings(), val loading: Boolean = true)
|
||||
|
|
|
@ -11,15 +11,16 @@ import kotlinx.coroutines.flow.stateIn
|
|||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SupportViewModel @Inject constructor(
|
||||
private val settingsRepository: SettingsRepository
|
||||
) : ViewModel() {
|
||||
class SupportViewModel @Inject constructor(private val settingsRepository: SettingsRepository) :
|
||||
ViewModel() {
|
||||
|
||||
val uiState = settingsRepository.getSettingsFlow().map {
|
||||
SupportUiState(it, false)
|
||||
}.stateIn(
|
||||
val uiState =
|
||||
settingsRepository
|
||||
.getSettingsFlow()
|
||||
.map { SupportUiState(it, false) }
|
||||
.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
||||
SupportUiState()
|
||||
SupportUiState(),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ private val DarkColorScheme =
|
|||
primary = virdigris,
|
||||
secondary = virdigris,
|
||||
// secondary = PurpleGrey80,
|
||||
tertiary = virdigris
|
||||
tertiary = virdigris,
|
||||
// tertiary = Pink80
|
||||
)
|
||||
|
||||
|
@ -29,7 +29,7 @@ private val LightColorScheme =
|
|||
lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
tertiary = Pink40,
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
|
@ -57,7 +57,6 @@ fun WireguardAutoTunnelTheme(
|
|||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
@ -68,14 +67,19 @@ fun WireguardAutoTunnelTheme(
|
|||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
window.statusBarColor = Color.Transparent.toArgb()
|
||||
window.navigationBarColor = Color.Transparent.toArgb()
|
||||
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = !darkTheme
|
||||
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightNavigationBars = !darkTheme
|
||||
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars =
|
||||
!darkTheme
|
||||
WindowCompat.getInsetsController(
|
||||
window,
|
||||
window.decorView,
|
||||
)
|
||||
.isAppearanceLightNavigationBars = !darkTheme
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ fun TransparentSystemBars() {
|
|||
DisposableEffect(systemUiController, useDarkIcons) {
|
||||
systemUiController.setSystemBarsColor(
|
||||
color = Color.Transparent,
|
||||
darkIcons = useDarkIcons
|
||||
darkIcons = useDarkIcons,
|
||||
)
|
||||
|
||||
onDispose {}
|
||||
|
|
|
@ -15,8 +15,8 @@ val Typography =
|
|||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
letterSpacing = 0.5.sp,
|
||||
),
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
|
|
|
@ -12,76 +12,97 @@ sealed class Event {
|
|||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_none)
|
||||
}
|
||||
|
||||
data object SsidConflict : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_ssid_exists)
|
||||
}
|
||||
|
||||
data object RootDenied : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_root_denied)
|
||||
}
|
||||
|
||||
data class General(val customMessage: String) : Error() {
|
||||
override val message: String
|
||||
get() = customMessage
|
||||
}
|
||||
|
||||
data class Exception(val exception: kotlin.Exception) : Error() {
|
||||
override val message: String
|
||||
get() = exception.message ?: WireGuardAutoTunnel.instance.getString(R.string.unknown_error)
|
||||
get() =
|
||||
exception.message
|
||||
?: WireGuardAutoTunnel.instance.getString(R.string.unknown_error)
|
||||
}
|
||||
|
||||
data object InvalidQrCode : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_invalid_code)
|
||||
}
|
||||
|
||||
data object InvalidFileExtension : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
|
||||
}
|
||||
|
||||
data object FileReadFailed : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
|
||||
}
|
||||
|
||||
data object AuthenticationFailed : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authentication_failed)
|
||||
}
|
||||
|
||||
data object AuthorizationFailed : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authorization_failed)
|
||||
}
|
||||
|
||||
data object BackgroundLocationRequired : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.background_location_required)
|
||||
get() =
|
||||
WireGuardAutoTunnel.instance.getString(R.string.background_location_required)
|
||||
}
|
||||
|
||||
data object LocationServicesRequired : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.location_services_required)
|
||||
}
|
||||
|
||||
data object PreciseLocationRequired : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.precise_location_required)
|
||||
}
|
||||
|
||||
data object FileExplorerRequired : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_no_file_explorer)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Message : Event() {
|
||||
data object ConfigSaved : Message() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.config_changes_saved)
|
||||
}
|
||||
|
||||
data object ConfigsExported : Message() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.exported_configs_message)
|
||||
}
|
||||
|
||||
data object TunnelOffAction : Message() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_tunnel)
|
||||
}
|
||||
|
||||
data object TunnelOnAction : Message() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_on_tunnel)
|
||||
}
|
||||
|
||||
data object AutoTunnelOffAction : Message() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_auto)
|
||||
|
|
|
@ -37,15 +37,15 @@ fun BigDecimal.toThreeDecimalPlaceString(): String {
|
|||
}
|
||||
|
||||
fun <T> List<T>.update(index: Int, item: T): List<T> = toMutableList().apply { this[index] = item }
|
||||
|
||||
fun <T> List<T>.removeAt(index: Int): List<T> = toMutableList().apply { this.removeAt(index) }
|
||||
|
||||
typealias TunnelConfigs = List<TunnelConfig>
|
||||
|
||||
typealias Packages = List<PackageInfo>
|
||||
|
||||
fun Statistics.mapPeerStats(): Map<Key, PeerStats?> {
|
||||
return this.peers().associateWith { key ->
|
||||
(this.peer(key))
|
||||
}
|
||||
return this.peers().associateWith { key -> (this.peer(key)) }
|
||||
}
|
||||
|
||||
fun PeerStats.latestHandshakeSeconds(): Long? {
|
||||
|
@ -65,4 +65,3 @@ fun PeerStats.handshakeStatus() : HandshakeStatus {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -36,21 +36,19 @@ object FileUtils {
|
|||
val target =
|
||||
File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||
fileName
|
||||
fileName,
|
||||
)
|
||||
return target.outputStream()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun saveFilesToZip(
|
||||
context: Context,
|
||||
files: List<File>
|
||||
) {
|
||||
val zipOutputStream = createDownloadsFileOutputStream(
|
||||
fun saveFilesToZip(context: Context, files: List<File>) {
|
||||
val zipOutputStream =
|
||||
createDownloadsFileOutputStream(
|
||||
context,
|
||||
"wg-export_${Instant.now().epochSecond}.zip",
|
||||
ZIP_FILE_MIME_TYPE
|
||||
ZIP_FILE_MIME_TYPE,
|
||||
)
|
||||
ZipOutputStream(zipOutputStream).use { zos ->
|
||||
files.forEach { file ->
|
||||
|
|
|
@ -2,9 +2,9 @@ package com.zaneschepke.wireguardautotunnel.util
|
|||
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
sealed class Result<T> {
|
||||
class Success<T>(val data: T) : Result<T>()
|
||||
|
||||
class Error<T>(val error: Event.Error) : Result<T>() {
|
||||
init {
|
||||
when (this.error) {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
android:viewportWidth="256"
|
||||
android:viewportHeight="256">
|
||||
<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: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>
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<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:fillType="evenOdd"
|
||||
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>
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<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 xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
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>
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<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 xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
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>
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<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 xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
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>
|
||||
|
|
|
@ -164,4 +164,7 @@
|
|||
<string name="location_service_missing">Location Services Not Detected</string>
|
||||
<string name="location_services_missing_message">The app is not detecting any location services enabled on your device. Depending on the device, this could cause the untrusted wifi feature to fail to read the wifi name. Would you like to continue anyways?</string>
|
||||
<string name="auto_tunnel_title">Auto-tunnel Service</string>
|
||||
<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>
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.WireguardAutoTunnel" parent="@style/Theme.AppCompat.NoActionBar">
|
||||
<item name="android:windowBackground">@color/black_background</item>
|
||||
</style>
|
||||
|
|
|
@ -1,31 +1,35 @@
|
|||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<shortcut
|
||||
android:shortcutId="defaultOn1"
|
||||
android:enabled="true"
|
||||
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:shortcutDisabledMessage="@string/vpn_on">
|
||||
android:shortcutShortLabel="@string/vpn_on">
|
||||
<intent
|
||||
android:action="START"
|
||||
android:targetPackage="com.zaneschepke.wireguardautotunnel"
|
||||
android:targetClass="com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsActivity">
|
||||
<extra android:name="className" android:value="WireGuardTunnelService" />
|
||||
android:targetClass="com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsActivity"
|
||||
android:targetPackage="com.zaneschepke.wireguardautotunnel">
|
||||
<extra
|
||||
android:name="className"
|
||||
android:value="WireGuardTunnelService" />
|
||||
</intent>
|
||||
<capability-binding android:key="actions.intent.START" />
|
||||
</shortcut>
|
||||
<shortcut
|
||||
android:shortcutId="defaultOff1"
|
||||
android:enabled="true"
|
||||
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:shortcutDisabledMessage="@string/vpn_off">
|
||||
android:shortcutShortLabel="@string/vpn_off">
|
||||
<intent
|
||||
android:action="STOP"
|
||||
android:targetPackage="com.zaneschepke.wireguardautotunnel"
|
||||
android:targetClass="com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsActivity">
|
||||
<extra android:name="className" android:value="WireGuardTunnelService" />
|
||||
android:targetClass="com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsActivity"
|
||||
android:targetPackage="com.zaneschepke.wireguardautotunnel">
|
||||
<extra
|
||||
android:name="className"
|
||||
android:value="WireGuardTunnelService" />
|
||||
</intent>
|
||||
<capability-binding android:key="actions.intent.STOP" />
|
||||
</shortcut>
|
||||
|
|
|
@ -26,11 +26,9 @@ object BuildHelper {
|
|||
}
|
||||
|
||||
fun isReleaseBuild(gradle: Gradle): Boolean {
|
||||
return (
|
||||
gradle.startParameter.taskNames.size > 0 &&
|
||||
return (gradle.startParameter.taskNames.size > 0 &&
|
||||
gradle.startParameter.taskNames[0].contains(
|
||||
"Release",
|
||||
)
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
object Constants {
|
||||
const val VERSION_NAME = "3.3.2"
|
||||
const val VERSION_NAME = "3.3.3"
|
||||
const val JVM_TARGET = "17"
|
||||
const val VERSION_CODE = 33200
|
||||
const val VERSION_CODE = 33300
|
||||
const val TARGET_SDK = 34
|
||||
const val MIN_SDK = 26
|
||||
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
||||
const val APP_NAME = "wgtunnel"
|
||||
const val COMPOSE_COMPILER_EXTENSION_VERSION = "1.5.7"
|
||||
|
||||
|
||||
const val STORE_PASS_VAR = "SIGNING_STORE_PASSWORD"
|
||||
const val KEY_ALIAS_VAR = "SIGNING_KEY_ALIAS"
|
||||
|
|
|
@ -5,13 +5,13 @@ platform :android do
|
|||
desc "Deploy a beta version to the Google Play"
|
||||
lane :beta do
|
||||
gradle(task: "clean bundleGeneralRelease")
|
||||
upload_to_play_store(track: 'beta')
|
||||
upload_to_play_store(track: 'beta', skip_upload_apk: true)
|
||||
end
|
||||
|
||||
desc "Deploy a new version to the Google Play"
|
||||
lane :production do
|
||||
gradle(task: "clean bundleGeneralRelease")
|
||||
upload_to_play_store
|
||||
upload_to_play_store(skip_upload_apk: true)
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
Enhancements:
|
||||
- Added delete tunnel confirmation
|
||||
- Added battery background permission
|
||||
Fixes:
|
||||
- Tunnel disable frozen bug
|
||||
- Email to field bug
|
||||
- Config edit empty DNS
|
|
@ -11,7 +11,7 @@ desugar_jdk_libs = "2.0.4"
|
|||
espressoCore = "3.5.1"
|
||||
firebase-crashlytics-gradle = "2.9.9"
|
||||
google-services = "4.4.0"
|
||||
hiltAndroid = "2.49"
|
||||
hiltAndroid = "2.50"
|
||||
hiltNavigationCompose = "1.1.0"
|
||||
junit = "4.13.2"
|
||||
kotlinx-serialization-json = "1.6.2"
|
||||
|
@ -22,15 +22,14 @@ navigationCompose = "2.7.6"
|
|||
roomVersion = "2.6.1"
|
||||
timber = "5.0.1"
|
||||
tunnel = "1.0.20230706"
|
||||
androidGradlePlugin = "8.2.0"
|
||||
kotlin="1.9.10"
|
||||
ksp="1.9.10-1.0.13"
|
||||
androidGradlePlugin = "8.2.1"
|
||||
kotlin = "1.9.21"
|
||||
ksp = "1.9.21-1.0.16"
|
||||
composeBom = "2023.10.01"
|
||||
firebaseBom = "32.7.0"
|
||||
compose = "1.5.4"
|
||||
crashlytics = "18.6.0"
|
||||
analytics = "21.5.0"
|
||||
composeCompiler="1.5.3"
|
||||
zxingAndroidEmbedded = "4.3.0"
|
||||
zxingCore = "3.5.2"
|
||||
|
||||
|
@ -84,7 +83,6 @@ lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-com
|
|||
material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "material-icons-extended" }
|
||||
|
||||
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
|
||||
|
||||
tunnel = { module = "com.wireguard.android:tunnel", version.ref = "tunnel" }
|
||||
|
||||
#firebase
|
||||
|
|
|
@ -5,6 +5,7 @@ pluginManagement {
|
|||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
|
@ -14,4 +15,5 @@ dependencyResolutionManagement {
|
|||
}
|
||||
|
||||
rootProject.name = "WG Tunnel"
|
||||
|
||||
include(":app")
|
||||
|
|
Loading…
Reference in New Issue