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:
Zane Schepke 2024-01-06 04:06:21 -05:00
parent 7ec294b789
commit 5a15776bb3
92 changed files with 3454 additions and 2930 deletions

85
.editorconfig Normal file
View File

@ -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

View File

@ -11,12 +11,14 @@ assignees: zaneschepke
A clear and concise description of what the bug is. A clear and concise description of what the bug is.
**Smartphone (please complete the following information):** **Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- Android Version: [e.g. iOS8.1] - Device: [e.g. Pixel 4a]
- App Version [e.g. 22] - Android Version: [e.g. Android 13]
- App Version [e.g. 3.3.3]
**To Reproduce** **To Reproduce**
Steps to reproduce the behavior: Steps to reproduce the behavior:
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
3. Scroll down to '....' 3. Scroll down to '....'

View File

@ -9,7 +9,7 @@ on:
jobs: jobs:
build: build:
name: Build Signed APK name: Build Signed APK
# change to macos because of hilt issues on ubuntu in gradle 8.3
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
@ -70,7 +70,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
# fix hardcode changelog file name # fix hardcode changelog file name
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/33200.txt body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/33300.txt
tag_name: ${{ github.ref_name }} tag_name: ${{ github.ref_name }}
name: Release ${{ github.ref_name }} name: Release ${{ github.ref_name }}
draft: false draft: false

View File

@ -28,7 +28,10 @@ WG Tunnel
<div align="left"> <div align="left">
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) with added features. Built using the [wireguard-android](https://github.com/WireGuard/wireguard-android) library and [Jetpack Compose](https://developer.android.com/jetpack/compose), this application was inspired by the official [WireGuard Android](https://github.com/WireGuard/wireguard-android) app. This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) with added
features. Built using the [wireguard-android](https://github.com/WireGuard/wireguard-android)
library and [Jetpack Compose](https://developer.android.com/jetpack/compose), this application was
inspired by the official [WireGuard Android](https://github.com/WireGuard/wireguard-android) app.
</div> </div>
@ -47,7 +50,8 @@ This is an alternative Android Application for [WireGuard](https://www.wireguard
## Inspiration ## Inspiration
The original inspiration for this app came from the inconvenience of having to manually turn VPN off and on while on different networks. This app was created to offer a free solution to this problem. The original inspiration for this app came from the inconvenience of having to manually turn VPN off
and on while on different networks. This app was created to offer a free solution to this problem.
## Features ## Features
@ -63,7 +67,6 @@ The original inspiration for this app came from the inconvenience of having to m
* Automatic service restart after reboot * Automatic service restart after reboot
* Battery preservation measures * Battery preservation measures
## Building ## Building
``` ```

View File

@ -19,9 +19,7 @@ android {
versionCode = Constants.VERSION_CODE versionCode = Constants.VERSION_CODE
versionName = Constants.VERSION_NAME versionName = Constants.VERSION_NAME
ksp { ksp { arg("room.schemaLocation", "$projectDir/schemas") }
arg("room.schemaLocation", "$projectDir/schemas")
}
sourceSets { sourceSets {
getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room
@ -30,9 +28,7 @@ android {
resourceConfigurations.addAll(listOf("en")) resourceConfigurations.addAll(listOf("en"))
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables { useSupportLibrary = true }
useSupportLibrary = true
}
} }
signingConfigs { signingConfigs {
@ -47,24 +43,33 @@ android {
} }
} }
// try to get secrets from env first for pipeline build, then properties file for local build // try to get secrets from env first for pipeline build, then properties file for local
storeFile = file( // build
System.getenv().getOrDefault( storeFile =
file(
System.getenv()
.getOrDefault(
Constants.KEY_STORE_PATH_VAR, Constants.KEY_STORE_PATH_VAR,
properties.getProperty(Constants.KEY_STORE_PATH_VAR) properties.getProperty(Constants.KEY_STORE_PATH_VAR),
),
) )
) storePassword =
storePassword = System.getenv().getOrDefault( System.getenv()
.getOrDefault(
Constants.STORE_PASS_VAR, Constants.STORE_PASS_VAR,
properties.getProperty(Constants.STORE_PASS_VAR) properties.getProperty(Constants.STORE_PASS_VAR),
) )
keyAlias = System.getenv().getOrDefault( keyAlias =
System.getenv()
.getOrDefault(
Constants.KEY_ALIAS_VAR, Constants.KEY_ALIAS_VAR,
properties.getProperty(Constants.KEY_ALIAS_VAR) properties.getProperty(Constants.KEY_ALIAS_VAR),
) )
keyPassword = System.getenv().getOrDefault( keyPassword =
System.getenv()
.getOrDefault(
Constants.KEY_PASS_VAR, Constants.KEY_PASS_VAR,
properties.getProperty(Constants.KEY_PASS_VAR) properties.getProperty(Constants.KEY_PASS_VAR),
) )
} }
} }
@ -72,7 +77,7 @@ android {
buildTypes { buildTypes {
// don't strip // don't strip
packaging.jniLibs.keepDebugSymbols.addAll( packaging.jniLibs.keepDebugSymbols.addAll(
listOf("libwg-go.so", "libwg-quick.so", "libwg.so") listOf("libwg-go.so", "libwg-quick.so", "libwg.so"),
) )
applicationVariants.all { applicationVariants.all {
@ -91,13 +96,11 @@ android {
isShrinkResources = true isShrinkResources = true
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "proguard-rules.pro",
) )
signingConfig = signingConfigs.getByName(Constants.RELEASE) signingConfig = signingConfigs.getByName(Constants.RELEASE)
} }
debug { debug { isDebuggable = true }
isDebuggable = true
}
} }
flavorDimensions.add(Constants.TYPE) flavorDimensions.add(Constants.TYPE)
productFlavors { productFlavors {
@ -118,24 +121,17 @@ android {
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true isCoreLibraryDesugaringEnabled = true
} }
kotlinOptions { kotlinOptions { jvmTarget = Constants.JVM_TARGET }
jvmTarget = Constants.JVM_TARGET
}
buildFeatures { buildFeatures {
compose = true compose = true
buildConfig = true buildConfig = true
} }
composeOptions { composeOptions { kotlinCompilerExtensionVersion = Constants.COMPOSE_COMPILER_EXTENSION_VERSION }
kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get() packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
} }
val generalImplementation by configurations val generalImplementation by configurations
dependencies { dependencies {
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)

View File

@ -14,9 +14,10 @@ class MigrationTest {
private val dbName = "migration-test" private val dbName = "migration-test"
@get:Rule @get:Rule
val helper: MigrationTestHelper = MigrationTestHelper( val helper: MigrationTestHelper =
MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(), InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java AppDatabase::class.java,
) )
@Test @Test
@ -50,11 +51,10 @@ class MigrationTest {
"'false'," + "'false'," +
"'false'," + "'false'," +
"'false'," + "'false'," +
"'false')" "'false')",
) )
execSQL( execSQL(
"INSERT INTO TunnelConfig (name, wg_quick)" + "INSERT INTO TunnelConfig (name, wg_quick)" + " VALUES ('hello', 'hello')",
" VALUES ('hello', 'hello')"
) )
// Prepare for the next version. // Prepare for the next version.
close() close()

View File

@ -1,14 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" <uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" /> android:maxSdkVersion="32" />
<uses-permission <uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" android:maxSdkVersion="32"
tools:ignore="ScopedStorage" /> tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" <uses-permission
android:name="android.permission.ACCESS_WIFI_STATE"
android:maxSdkVersion="30" android:maxSdkVersion="30"
tools:ignore="LeanbackUsesWifi" /> tools:ignore="LeanbackUsesWifi" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
@ -25,12 +28,15 @@
<!--foreground service permissions--> <!--foreground service permissions-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<!--start service on boot permission--> <!--start service on boot permission-->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!--android tv support--> <!--android tv support-->
<uses-feature android:name="android.software.leanback" <uses-feature
android:name="android.software.leanback"
android:required="false" /> android:required="false" />
<uses-feature android:name="android.hardware.touchscreen" <uses-feature
android:name="android.hardware.touchscreen"
android:required="false" /> android:required="false" />
<uses-feature <uses-feature
android:name="android.hardware.location.gps" android:name="android.hardware.location.gps"
@ -38,19 +44,20 @@
<uses-feature <uses-feature
android:name="android.hardware.screen.portrait" android:name="android.hardware.screen.portrait"
android:required="false" /> android:required="false" />
<queries> <queries>
<intent> <intent>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
</intent> </intent>
</queries> </queries>
<application <application
android:allowBackup="true"
android:enableOnBackInvokedCallback="true"
android:name=".WireGuardAutoTunnel" android:name=".WireGuardAutoTunnel"
android:allowBackup="true"
android:banner="@mipmap/ic_banner"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:banner="@mipmap/ic_banner"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
@ -62,11 +69,14 @@
android:theme="@style/Theme.WireguardAutoTunnel"> android:theme="@style/Theme.WireguardAutoTunnel">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" /> <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" /> <action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter> </intent-filter>
<meta-data android:name="android.app.shortcuts" <meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" /> android:resource="@xml/shortcuts" />
</activity> </activity>
<activity <activity
@ -76,65 +86,74 @@
android:theme="@style/zxing_CaptureTheme" android:theme="@style/zxing_CaptureTheme"
android:windowSoftInputMode="stateAlwaysHidden" /> android:windowSoftInputMode="stateAlwaysHidden" />
<activity <activity
android:finishOnTaskLaunch="true" android:name=".service.shortcut.ShortcutsActivity"
android:enabled="true" android:enabled="true"
android:exported="true" android:exported="true"
android:theme="@android:style/Theme.NoDisplay" android:finishOnTaskLaunch="true"
android:name=".service.shortcut.ShortcutsActivity"/> android:theme="@android:style/Theme.NoDisplay" />
<service <service
android:name=".service.foreground.ForegroundService" android:name=".service.foreground.ForegroundService"
android:enabled="true" android:enabled="true"
android:exported="false"
android:foregroundServiceType="systemExempted|specialUse" android:foregroundServiceType="systemExempted|specialUse"
tools:node="merge" tools:node="merge" />
android:exported="false">
</service>
<service <service
android:exported="true"
android:name=".service.tile.TunnelControlTile" android:name=".service.tile.TunnelControlTile"
android:exported="true"
android:icon="@drawable/shield" android:icon="@drawable/shield"
android:label="WG Tunnel" android:label="WG Tunnel"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"> android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data android:name="android.service.quicksettings.ACTIVE_TILE" <meta-data
android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="true" /> android:value="true" />
<meta-data android:name="android.service.quicksettings.TOGGLEABLE_TILE" <meta-data
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" /> android:value="true" />
<intent-filter> <intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" /> <action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter> </intent-filter>
</service> </service>
<service <service
android:name=".service.foreground.WireGuardTunnelService" android:name=".service.foreground.WireGuardTunnelService"
android:permission="android.permission.BIND_VPN_SERVICE"
android:enabled="true" android:enabled="true"
android:persistent="true" android:exported="false"
android:foregroundServiceType="systemExempted|specialUse" android:foregroundServiceType="systemExempted|specialUse"
tools:node="merge" android:permission="android.permission.BIND_VPN_SERVICE"
android:exported="false"> android:persistent="true"
tools:node="merge">
<intent-filter> <intent-filter>
<action android:name="android.net.VpnService" /> <action android:name="android.net.VpnService" />
</intent-filter> </intent-filter>
<meta-data android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON" <meta-data
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
android:value="true" /> android:value="true" />
</service> </service>
<service <service
android:name=".service.foreground.WireGuardConnectivityWatcherService" android:name=".service.foreground.WireGuardConnectivityWatcherService"
android:enabled="true" android:enabled="true"
android:stopWithTask="false" android:exported="false"
android:persistent="true"
android:foregroundServiceType="systemExempted|specialUse" android:foregroundServiceType="systemExempted|specialUse"
tools:node="merge" android:persistent="true"
android:exported="false"> android:stopWithTask="false"
</service> tools:node="merge" />
<receiver android:enabled="true" android:name=".receiver.BootReceiver"
<receiver
android:name=".receiver.BootReceiver"
android:enabled="true"
android:exported="false"> android:exported="false">
<intent-filter> <intent-filter>
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" /> <action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" /> <action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" /> <action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:exported="false" android:name=".receiver.NotificationActionReceiver"/> <receiver
android:name=".receiver.NotificationActionReceiver"
android:exported="false" />
</application> </application>
</manifest> </manifest>

View File

@ -4,61 +4,31 @@ import android.app.Application
import android.content.ComponentName import android.content.ComponentName
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.io.IOException
import javax.inject.Inject
@HiltAndroidApp @HiltAndroidApp
class WireGuardAutoTunnel : Application() { class WireGuardAutoTunnel : Application() {
@Inject
lateinit var settingsRepository: SettingsRepository
@Inject
lateinit var dataStoreManager: DataStoreManager
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
instance = this instance = this
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
initSettings()
with(ProcessLifecycleOwner.get()) {
lifecycleScope.launch {
try {
// load preferences into memory
dataStoreManager.init()
requestTileServiceStateUpdate()
} catch (e: IOException) {
Timber.e("Failed to load preferences")
}
}
}
}
private fun initSettings() {
with(ProcessLifecycleOwner.get()) {
lifecycleScope.launch {
if (settingsRepository.getAll().isEmpty()) {
settingsRepository.save(Settings())
}
}
}
} }
companion object { companion object {
lateinit var instance: WireGuardAutoTunnel private set lateinit var instance: WireGuardAutoTunnel
private set
fun isRunningOnAndroidTv(): Boolean { fun isRunningOnAndroidTv(): Boolean {
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
} }
fun requestTileServiceStateUpdate() { fun requestTileServiceStateUpdate() {
TileService.requestListeningState(instance, ComponentName(instance, TunnelControlTile::class.java)) TileService.requestListeningState(
instance,
ComponentName(instance, TunnelControlTile::class.java),
)
} }
} }
} }

View File

@ -10,16 +10,20 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
@Database( @Database(
entities = [Settings::class, TunnelConfig::class], entities = [Settings::class, TunnelConfig::class],
version = 5, version = 5,
autoMigrations = [ autoMigrations =
AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3), AutoMigration( [
AutoMigration(from = 1, to = 2),
AutoMigration(from = 2, to = 3),
AutoMigration(
from = 3, from = 3,
to = 4 to = 4,
),AutoMigration( ),
AutoMigration(
from = 4, from = 4,
to = 5 to = 5,
) ),
], ],
exportSchema = true exportSchema = true,
) )
@TypeConverters(DatabaseListConverters::class) @TypeConverters(DatabaseListConverters::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {

View File

@ -10,27 +10,19 @@ import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface SettingsDao { interface SettingsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: Settings)
suspend fun save(t: Settings)
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<Settings>)
suspend fun saveAll(t: List<Settings>)
@Query("SELECT * FROM settings WHERE id=:id") @Query("SELECT * FROM settings WHERE id=:id") suspend fun getById(id: Long): Settings?
suspend fun getById(id: Long): Settings?
@Query("SELECT * FROM settings") @Query("SELECT * FROM settings") suspend fun getAll(): List<Settings>
suspend fun getAll(): List<Settings>
@Query("SELECT * FROM settings LIMIT 1") @Query("SELECT * FROM settings LIMIT 1") fun getSettingsFlow(): Flow<Settings>
fun getSettingsFlow(): Flow<Settings>
@Query("SELECT * FROM settings") @Query("SELECT * FROM settings") fun getAllFlow(): Flow<MutableList<Settings>>
fun getAllFlow(): Flow<MutableList<Settings>>
@Delete @Delete suspend fun delete(t: Settings)
suspend fun delete(t: Settings)
@Query("SELECT COUNT('id') FROM settings") @Query("SELECT COUNT('id') FROM settings") suspend fun count(): Long
suspend fun count(): Long
} }

View File

@ -10,24 +10,17 @@ import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface TunnelConfigDao { interface TunnelConfigDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: TunnelConfig)
suspend fun save(t: TunnelConfig)
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<TunnelConfig>)
suspend fun saveAll(t: List<TunnelConfig>)
@Query("SELECT * FROM TunnelConfig WHERE id=:id") @Query("SELECT * FROM TunnelConfig WHERE id=:id") suspend fun getById(id: Long): TunnelConfig?
suspend fun getById(id: Long): TunnelConfig?
@Query("SELECT * FROM TunnelConfig") @Query("SELECT * FROM TunnelConfig") suspend fun getAll(): List<TunnelConfig>
suspend fun getAll(): List<TunnelConfig>
@Delete @Delete suspend fun delete(t: TunnelConfig)
suspend fun delete(t: TunnelConfig)
@Query("SELECT COUNT('id') FROM TunnelConfig") @Query("SELECT COUNT('id') FROM TunnelConfig") suspend fun count(): Long
suspend fun count(): Long
@Query("SELECT * FROM tunnelconfig") @Query("SELECT * FROM tunnelconfig") fun getAllFlow(): Flow<MutableList<TunnelConfig>>
fun getAllFlow(): Flow<MutableList<TunnelConfig>>
} }

View File

@ -1,4 +1,5 @@
package com.zaneschepke.wireguardautotunnel.data.datastore package com.zaneschepke.wireguardautotunnel.data.datastore
import android.content.Context import android.content.Context
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.booleanPreferencesKey
@ -11,12 +12,14 @@ import kotlinx.coroutines.flow.map
class DataStoreManager(private val context: Context) { class DataStoreManager(private val context: Context) {
companion object { companion object {
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN") val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
} }
// preferences // preferences
private val preferencesKey = "preferences" private val preferencesKey = "preferences"
private val Context.dataStore by preferencesDataStore( private val Context.dataStore by
name = preferencesKey preferencesDataStore(
name = preferencesKey,
) )
suspend fun init() { suspend fun init() {
@ -24,16 +27,12 @@ class DataStoreManager(private val context: Context) {
} }
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) = suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) =
context.dataStore.edit { context.dataStore.edit { it[key] = value }
it[key] = value
}
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map {
it[key]
}
suspend fun <T> getFromStore(key: Preferences.Key<T>) = context.dataStore.data.first { it.contains(key) }[key] fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] }
val locationDisclosureFlow: Flow<Boolean?> = context.dataStore.data.map { suspend fun <T> getFromStore(key: Preferences.Key<T>) =
it[LOCATION_DISCLOSURE_SHOWN] context.dataStore.data.first { it.contains(key) }[key]
}
val preferencesFlow: Flow<Preferences?> = context.dataStore.data
} }

View File

@ -8,39 +8,49 @@ import androidx.room.PrimaryKey
data class Settings( data class Settings(
@PrimaryKey(autoGenerate = true) val id: Int = 0, @PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "is_tunnel_enabled") var isAutoTunnelEnabled: Boolean = false, @ColumnInfo(name = "is_tunnel_enabled") var isAutoTunnelEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled") var isTunnelOnMobileDataEnabled: Boolean = false, @ColumnInfo(name = "is_tunnel_on_mobile_data_enabled")
@ColumnInfo(name = "trusted_network_ssids") var trustedNetworkSSIDs: MutableList<String> = mutableListOf(), var isTunnelOnMobileDataEnabled: Boolean = false,
@ColumnInfo(name = "trusted_network_ssids")
var trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
@ColumnInfo(name = "default_tunnel") var defaultTunnel: String? = null, @ColumnInfo(name = "default_tunnel") var defaultTunnel: String? = null,
@ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled: Boolean = false, @ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled") var isTunnelOnEthernetEnabled: Boolean = false, @ColumnInfo(name = "is_tunnel_on_ethernet_enabled")
var isTunnelOnEthernetEnabled: Boolean = false,
@ColumnInfo( @ColumnInfo(
name = "is_shortcuts_enabled", name = "is_shortcuts_enabled",
defaultValue = "false" defaultValue = "false",
) var isShortcutsEnabled: Boolean = false, )
var isShortcutsEnabled: Boolean = false,
@ColumnInfo( @ColumnInfo(
name = "is_battery_saver_enabled", name = "is_battery_saver_enabled",
defaultValue = "false" defaultValue = "false",
) var isBatterySaverEnabled: Boolean = false, )
var isBatterySaverEnabled: Boolean = false,
@ColumnInfo( @ColumnInfo(
name = "is_tunnel_on_wifi_enabled", name = "is_tunnel_on_wifi_enabled",
defaultValue = "false" defaultValue = "false",
) var isTunnelOnWifiEnabled: Boolean = false, )
var isTunnelOnWifiEnabled: Boolean = false,
@ColumnInfo( @ColumnInfo(
name = "is_kernel_enabled", name = "is_kernel_enabled",
defaultValue = "false" defaultValue = "false",
) var isKernelEnabled: Boolean = false, )
var isKernelEnabled: Boolean = false,
@ColumnInfo( @ColumnInfo(
name = "is_restore_on_boot_enabled", name = "is_restore_on_boot_enabled",
defaultValue = "false" defaultValue = "false",
) var isRestoreOnBootEnabled: Boolean = false, )
var isRestoreOnBootEnabled: Boolean = false,
@ColumnInfo( @ColumnInfo(
name = "is_multi_tunnel_enabled", name = "is_multi_tunnel_enabled",
defaultValue = "false" defaultValue = "false",
) var isMultiTunnelEnabled: Boolean = false, )
var isMultiTunnelEnabled: Boolean = false,
@ColumnInfo( @ColumnInfo(
name = "is_auto_tunnel_paused", name = "is_auto_tunnel_paused",
defaultValue = "false" defaultValue = "false",
) var isAutoTunnelPaused: Boolean = false, )
var isAutoTunnelPaused: Boolean = false,
) { ) {
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean { fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean {
return if (defaultTunnel != null) { return if (defaultTunnel != null) {

View File

@ -5,8 +5,10 @@ import kotlinx.coroutines.flow.Flow
interface SettingsRepository { interface SettingsRepository {
suspend fun save(settings: Settings) suspend fun save(settings: Settings)
fun getSettingsFlow(): Flow<Settings> fun getSettingsFlow(): Flow<Settings>
suspend fun getSettings(): Settings suspend fun getSettings(): Settings
suspend fun getAll(): List<Settings> suspend fun getAll(): List<Settings>
} }

View File

@ -7,8 +7,12 @@ import kotlinx.coroutines.flow.Flow
interface TunnelConfigRepository { interface TunnelConfigRepository {
fun getTunnelConfigsFlow(): Flow<TunnelConfigs> fun getTunnelConfigsFlow(): Flow<TunnelConfigs>
suspend fun getAll(): TunnelConfigs suspend fun getAll(): TunnelConfigs
suspend fun save(tunnelConfig: TunnelConfig) suspend fun save(tunnelConfig: TunnelConfig)
suspend fun delete(tunnelConfig: TunnelConfig) suspend fun delete(tunnelConfig: TunnelConfig)
suspend fun count(): Int suspend fun count(): Int
} }

View File

@ -5,7 +5,8 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
class TunnelConfigRepositoryImpl(private val tunnelConfigDao: TunnelConfigDao) : TunnelConfigRepository { class TunnelConfigRepositoryImpl(private val tunnelConfigDao: TunnelConfigDao) :
TunnelConfigRepository {
override fun getTunnelConfigsFlow(): Flow<TunnelConfigs> { override fun getTunnelConfigsFlow(): Flow<TunnelConfigs> {
return tunnelConfigDao.getAllFlow() return tunnelConfigDao.getAllFlow()
} }

View File

@ -16,13 +16,11 @@ import javax.inject.Singleton
class DatabaseModule { class DatabaseModule {
@Provides @Provides
@Singleton @Singleton
fun provideDatabase( fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
@ApplicationContext context: Context
): AppDatabase {
return Room.databaseBuilder( return Room.databaseBuilder(
context, context,
AppDatabase::class.java, AppDatabase::class.java,
context.getString(R.string.db_name) context.getString(R.string.db_name),
) )
.fallbackToDestructiveMigration() .fallbackToDestructiveMigration()
.build() .build()

View File

@ -2,6 +2,4 @@ package com.zaneschepke.wireguardautotunnel.module
import javax.inject.Qualifier import javax.inject.Qualifier
@Qualifier @Qualifier @Retention(AnnotationRetention.BINARY) annotation class Kernel
@Retention(AnnotationRetention.BINARY)
annotation class Kernel

View File

@ -17,7 +17,9 @@ import dagger.hilt.android.scopes.ServiceScoped
abstract class ServiceModule { abstract class ServiceModule {
@Binds @Binds
@ServiceScoped @ServiceScoped
abstract fun provideNotificationService(wireGuardNotification: WireGuardNotification): NotificationService abstract fun provideNotificationService(
wireGuardNotification: WireGuardNotification
): NotificationService
@Binds @Binds
@ServiceScoped @ServiceScoped
@ -25,9 +27,13 @@ abstract class ServiceModule {
@Binds @Binds
@ServiceScoped @ServiceScoped
abstract fun provideMobileDataService(mobileDataService: MobileDataService): NetworkService<MobileDataService> abstract fun provideMobileDataService(
mobileDataService: MobileDataService
): NetworkService<MobileDataService>
@Binds @Binds
@ServiceScoped @ServiceScoped
abstract fun provideEthernetService(ethernetService: EthernetService): NetworkService<EthernetService> abstract fun provideEthernetService(
ethernetService: EthernetService
): NetworkService<EthernetService>
} }

View File

@ -21,28 +21,21 @@ import javax.inject.Singleton
class TunnelModule { class TunnelModule {
@Provides @Provides
@Singleton @Singleton
fun provideRootShell( fun provideRootShell(@ApplicationContext context: Context): RootShell {
@ApplicationContext context: Context
): RootShell {
return RootShell(context) return RootShell(context)
} }
@Provides @Provides
@Singleton @Singleton
@Userspace @Userspace
fun provideUserspaceBackend( fun provideUserspaceBackend(@ApplicationContext context: Context): Backend {
@ApplicationContext context: Context
): Backend {
return GoBackend(context) return GoBackend(context)
} }
@Provides @Provides
@Singleton @Singleton
@Kernel @Kernel
fun provideKernelBackend( fun provideKernelBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend {
@ApplicationContext context: Context,
rootShell: RootShell
): Backend {
return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell)) return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell))
} }

View File

@ -2,6 +2,4 @@ package com.zaneschepke.wireguardautotunnel.module
import javax.inject.Qualifier import javax.inject.Qualifier
@Qualifier @Qualifier @Retention(AnnotationRetention.BINARY) annotation class Userspace
@Retention(AnnotationRetention.BINARY)
annotation class Userspace

View File

@ -9,17 +9,15 @@ import com.zaneschepke.wireguardautotunnel.util.goAsync
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class BootReceiver : BroadcastReceiver() { class BootReceiver : BroadcastReceiver() {
@Inject @Inject lateinit var settingsRepository: SettingsRepository
lateinit var settingsRepository: SettingsRepository
override fun onReceive(context: Context?, intent: Intent?) = goAsync { override fun onReceive(context: Context?, intent: Intent?) = goAsync {
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
if (settingsRepository.getSettings().isAutoTunnelEnabled) { if (settingsRepository.getSettings().isAutoTunnelEnabled) {
ServiceManager.startWatcherServiceForeground(context!!) ServiceManager.startWatcherServiceForeground(context!!)
} }
} }
} }

View File

@ -14,13 +14,9 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class NotificationActionReceiver : BroadcastReceiver() { class NotificationActionReceiver : BroadcastReceiver() {
@Inject @Inject lateinit var settingsRepository: SettingsRepository
lateinit var settingsRepository: SettingsRepository
override fun onReceive( override fun onReceive(context: Context, intent: Intent?) = goAsync {
context: Context,
intent: Intent?
) = goAsync {
try { try {
val settings = settingsRepository.getSettings() val settings = settingsRepository.getSettings()
if (settings.defaultTunnel != null) { if (settings.defaultTunnel != null) {

View File

@ -15,18 +15,14 @@ open class ForegroundService : LifecycleService() {
return null return null
} }
override fun onStartCommand( override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
intent: Intent?,
flags: Int,
startId: Int
): Int {
super.onStartCommand(intent, flags, startId) super.onStartCommand(intent, flags, startId)
Timber.d("onStartCommand executed with startId: $startId") Timber.d("onStartCommand executed with startId: $startId")
if (intent != null) { if (intent != null) {
val action = intent.action val action = intent.action
Timber.d("using an intent with action $action")
when (action) { when (action) {
Action.START.name, Action.START_FOREGROUND.name -> startService(intent.extras) Action.START.name,
Action.START_FOREGROUND.name -> startService(intent.extras)
Action.STOP.name -> stopService(intent.extras) Action.STOP.name -> stopService(intent.extras)
"android.net.VpnService" -> { "android.net.VpnService" -> {
Timber.d("Always-on VPN starting service") Timber.d("Always-on VPN starting service")
@ -36,7 +32,7 @@ open class ForegroundService : LifecycleService() {
} }
} else { } else {
Timber.d( Timber.d(
"with a null intent. It has been probably restarted by the system." "with a null intent. It has been probably restarted by the system.",
) )
} }
// by returning this we make sure the service is restarted if the system kills the service // by returning this we make sure the service is restarted if the system kills the service

View File

@ -1,28 +1,27 @@
package com.zaneschepke.wireguardautotunnel.service.foreground package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.ActivityManager
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Context.ACTIVITY_SERVICE
import android.content.Intent import android.content.Intent
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import timber.log.Timber import timber.log.Timber
object ServiceManager { object ServiceManager {
@Suppress("DEPRECATION")
private // Deprecated for third party Services.
fun <T> Context.isServiceRunning(service: Class<T>) =
(getSystemService(ACTIVITY_SERVICE) as ActivityManager)
.getRunningServices(Integer.MAX_VALUE)
.any { it.service.className == service.name }
fun <T : Service> getServiceState( // private
context: Context, // fun <T> Context.isServiceRunning(service: Class<T>) =
cls: Class<T> // (getSystemService(ACTIVITY_SERVICE) as ActivityManager)
): ServiceState { // .runningAppProcesses.any {
val isServiceRunning = context.isServiceRunning(cls) // it.processName == service.name
return if (isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED // }
} //
// fun <T : Service> getServiceState(
// context: Context,
// cls: Class<T>
// ): ServiceState {
// val isServiceRunning = context.isServiceRunning(cls)
// return if (isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED
// }
private fun <T : Service> actionOnService( private fun <T : Service> actionOnService(
action: Action, action: Action,
@ -30,14 +29,10 @@ object ServiceManager {
cls: Class<T>, cls: Class<T>,
extras: Map<String, String>? = null extras: Map<String, String>? = null
) { ) {
if (getServiceState(context, cls) == ServiceState.STOPPED && action == Action.STOP) return
if (getServiceState(context, cls) == ServiceState.STARTED && action == Action.START) return
val intent = val intent =
Intent(context, cls).also { Intent(context, cls).also {
it.action = action.name it.action = action.name
extras?.forEach { (k, v) -> extras?.forEach { (k, v) -> it.putExtra(k, v) }
it.putExtra(k, v)
}
} }
intent.component?.javaClass intent.component?.javaClass
try { try {
@ -45,11 +40,9 @@ object ServiceManager {
Action.START_FOREGROUND -> { Action.START_FOREGROUND -> {
context.startForegroundService(intent) context.startForegroundService(intent)
} }
Action.START -> { Action.START -> {
context.startService(intent) context.startService(intent)
} }
Action.STOP -> context.startService(intent) Action.STOP -> context.startService(intent)
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -57,35 +50,30 @@ object ServiceManager {
} }
} }
fun startVpnService( fun startVpnService(context: Context, tunnelConfig: String) {
context: Context,
tunnelConfig: String
) {
actionOnService( actionOnService(
Action.START, Action.START,
context, context,
WireGuardTunnelService::class.java, WireGuardTunnelService::class.java,
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig) mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig),
) )
} }
fun stopVpnService(context: Context) { fun stopVpnService(context: Context) {
Timber.d("Stopping vpn service action")
actionOnService( actionOnService(
Action.STOP, Action.STOP,
context, context,
WireGuardTunnelService::class.java WireGuardTunnelService::class.java,
) )
} }
fun startVpnServiceForeground( fun startVpnServiceForeground(context: Context, tunnelConfig: String) {
context: Context,
tunnelConfig: String
) {
actionOnService( actionOnService(
Action.START_FOREGROUND, Action.START_FOREGROUND,
context, context,
WireGuardTunnelService::class.java, WireGuardTunnelService::class.java,
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig) mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig),
) )
} }
@ -95,17 +83,15 @@ object ServiceManager {
actionOnService( actionOnService(
Action.START_FOREGROUND, Action.START_FOREGROUND,
context, context,
WireGuardConnectivityWatcherService::class.java WireGuardConnectivityWatcherService::class.java,
) )
} }
fun startWatcherService( fun startWatcherService(context: Context) {
context: Context
) {
actionOnService( actionOnService(
Action.START, Action.START,
context, context,
WireGuardConnectivityWatcherService::class.java WireGuardConnectivityWatcherService::class.java,
) )
} }
@ -113,7 +99,7 @@ object ServiceManager {
actionOnService( actionOnService(
Action.STOP, Action.STOP,
context, context,
WireGuardConnectivityWatcherService::class.java WireGuardConnectivityWatcherService::class.java,
) )
} }
} }

View File

@ -49,6 +49,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
@Inject lateinit var vpnService: VpnService @Inject lateinit var vpnService: VpnService
private val networkEventsFlow = MutableStateFlow(WatcherState()) private val networkEventsFlow = MutableStateFlow(WatcherState())
data class WatcherState( data class WatcherState(
val isWifiConnected: Boolean = false, val isWifiConnected: Boolean = false,
val isVpnConnected: Boolean = false, val isVpnConnected: Boolean = false,
@ -99,15 +100,22 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
stopSelf() stopSelf()
} }
private fun launchWatcherNotification(description: String = getString(R.string.watcher_notification_text_active)) { private fun launchWatcherNotification(
description: String = getString(R.string.watcher_notification_text_active)
) {
val notification = val notification =
notificationService.createNotification( notificationService.createNotification(
channelId = getString(R.string.watcher_channel_id), channelId = getString(R.string.watcher_channel_id),
channelName = getString(R.string.watcher_channel_name), channelName = getString(R.string.watcher_channel_name),
title = getString(R.string.auto_tunnel_title), title = getString(R.string.auto_tunnel_title),
description = description) description = description,
)
ServiceCompat.startForeground( ServiceCompat.startForeground(
this, foregroundId, notification, Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID) this,
foregroundId,
notification,
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
)
} }
private fun launchWatcherPausedNotification() { private fun launchWatcherPausedNotification() {
@ -124,14 +132,16 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
this, this,
1, 1,
restartServiceIntent, restartServiceIntent,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE) PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
)
applicationContext.getSystemService(Context.ALARM_SERVICE) applicationContext.getSystemService(Context.ALARM_SERVICE)
val alarmService: AlarmManager = val alarmService: AlarmManager =
applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmService.set( alarmService.set(
AlarmManager.ELAPSED_REALTIME, AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime() + 1000, SystemClock.elapsedRealtime() + 1000,
restartServicePendingIntent) restartServicePendingIntent,
)
} }
private suspend fun initWakeLock() { private suspend fun initWakeLock() {
@ -199,25 +209,29 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
when (it) { when (it) {
is NetworkStatus.Available -> { is NetworkStatus.Available -> {
Timber.d("Gained Mobile data connection") Timber.d("Gained Mobile data connection")
networkEventsFlow.value = networkEventsFlow.value.copy( networkEventsFlow.value =
isMobileDataConnected = true networkEventsFlow.value.copy(
isMobileDataConnected = true,
) )
} }
is NetworkStatus.CapabilitiesChanged -> { is NetworkStatus.CapabilitiesChanged -> {
networkEventsFlow.value = networkEventsFlow.value.copy( networkEventsFlow.value =
isMobileDataConnected = true networkEventsFlow.value.copy(
isMobileDataConnected = true,
) )
Timber.d("Mobile data capabilities changed") Timber.d("Mobile data capabilities changed")
} }
is NetworkStatus.Unavailable -> { is NetworkStatus.Unavailable -> {
networkEventsFlow.value = networkEventsFlow.value.copy( networkEventsFlow.value =
isMobileDataConnected = false networkEventsFlow.value.copy(
isMobileDataConnected = false,
) )
Timber.d("Lost mobile data connection") Timber.d("Lost mobile data connection")
} }
} }
} }
} }
private suspend fun watchForSettingsChanges() { private suspend fun watchForSettingsChanges() {
settingsRepository.getSettingsFlow().collect { settingsRepository.getSettingsFlow().collect {
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) { if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
@ -226,8 +240,9 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
false -> launchWatcherNotification() false -> launchWatcherNotification()
} }
} }
networkEventsFlow.value = networkEventsFlow.value.copy( networkEventsFlow.value =
settings = it networkEventsFlow.value.copy(
settings = it,
) )
} }
} }
@ -235,11 +250,15 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
private suspend fun watchForVpnConnectivityChanges() { private suspend fun watchForVpnConnectivityChanges() {
vpnService.vpnState.collect { vpnService.vpnState.collect {
when (it.status) { when (it.status) {
Tunnel.State.DOWN -> networkEventsFlow.value = networkEventsFlow.value.copy( Tunnel.State.DOWN ->
isVpnConnected = false networkEventsFlow.value =
networkEventsFlow.value.copy(
isVpnConnected = false,
) )
Tunnel.State.UP -> networkEventsFlow.value = networkEventsFlow.value.copy( Tunnel.State.UP ->
isVpnConnected = true networkEventsFlow.value =
networkEventsFlow.value.copy(
isVpnConnected = true,
) )
else -> {} else -> {}
} }
@ -251,19 +270,22 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
when (it) { when (it) {
is NetworkStatus.Available -> { is NetworkStatus.Available -> {
Timber.d("Gained Ethernet connection") Timber.d("Gained Ethernet connection")
networkEventsFlow.value = networkEventsFlow.value.copy( networkEventsFlow.value =
isEthernetConnected = true networkEventsFlow.value.copy(
isEthernetConnected = true,
) )
} }
is NetworkStatus.CapabilitiesChanged -> { is NetworkStatus.CapabilitiesChanged -> {
Timber.d("Ethernet capabilities changed") Timber.d("Ethernet capabilities changed")
networkEventsFlow.value = networkEventsFlow.value.copy( networkEventsFlow.value =
isEthernetConnected = true networkEventsFlow.value.copy(
isEthernetConnected = true,
) )
} }
is NetworkStatus.Unavailable -> { is NetworkStatus.Unavailable -> {
networkEventsFlow.value = networkEventsFlow.value.copy( networkEventsFlow.value =
isEthernetConnected = false networkEventsFlow.value.copy(
isEthernetConnected = false,
) )
Timber.d("Lost Ethernet connection") Timber.d("Lost Ethernet connection")
} }
@ -276,24 +298,28 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
when (it) { when (it) {
is NetworkStatus.Available -> { is NetworkStatus.Available -> {
Timber.d("Gained Wi-Fi connection") Timber.d("Gained Wi-Fi connection")
networkEventsFlow.value = networkEventsFlow.value.copy( networkEventsFlow.value =
isWifiConnected = true networkEventsFlow.value.copy(
isWifiConnected = true,
) )
} }
is NetworkStatus.CapabilitiesChanged -> { is NetworkStatus.CapabilitiesChanged -> {
Timber.d("Wifi capabilities changed") Timber.d("Wifi capabilities changed")
networkEventsFlow.value = networkEventsFlow.value.copy( networkEventsFlow.value =
isWifiConnected = true networkEventsFlow.value.copy(
isWifiConnected = true,
) )
val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: "" val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: ""
Timber.d("Detected SSID: $ssid") Timber.d("Detected SSID: $ssid")
networkEventsFlow.value = networkEventsFlow.value.copy( networkEventsFlow.value =
currentNetworkSSID = ssid networkEventsFlow.value.copy(
currentNetworkSSID = ssid,
) )
} }
is NetworkStatus.Unavailable -> { is NetworkStatus.Unavailable -> {
networkEventsFlow.value = networkEventsFlow.value.copy( networkEventsFlow.value =
isWifiConnected = false networkEventsFlow.value.copy(
isWifiConnected = false,
) )
Timber.d("Lost Wi-Fi connection") Timber.d("Lost Wi-Fi connection")
} }
@ -338,7 +364,8 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
Timber.i("Condition 4 met") Timber.i("Condition 4 met")
} }
(!it.isEthernetConnected && (!it.isEthernetConnected &&
(it.isWifiConnected && it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) && (it.isWifiConnected &&
it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) &&
(it.isVpnConnected)) -> { (it.isVpnConnected)) -> {
ServiceManager.stopVpnService(this) ServiceManager.stopVpnService(this)
Timber.i("Condition 5 met") Timber.i("Condition 5 met")

View File

@ -28,17 +28,13 @@ import javax.inject.Inject
class WireGuardTunnelService : ForegroundService() { class WireGuardTunnelService : ForegroundService() {
private val foregroundId = 123 private val foregroundId = 123
@Inject @Inject lateinit var vpnService: VpnService
lateinit var vpnService: VpnService
@Inject @Inject lateinit var settingsRepository: SettingsRepository
lateinit var settingsRepository: SettingsRepository
@Inject @Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
lateinit var tunnelConfigRepository: TunnelConfigRepository
@Inject @Inject lateinit var notificationService: NotificationService
lateinit var notificationService: NotificationService
private lateinit var job: Job private lateinit var job: Job
@ -58,11 +54,10 @@ class WireGuardTunnelService : ForegroundService() {
super.startService(extras) super.startService(extras)
cancelJob() cancelJob()
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key)) val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
val tunnelConfig = tunnelConfigString?.let { val tunnelConfig = tunnelConfigString?.let { TunnelConfig.from(it) }
TunnelConfig.from(it)
}
tunnelName = tunnelConfig?.name ?: "" tunnelName = tunnelConfig?.name ?: ""
job = lifecycleScope.launch(Dispatchers.IO) { job =
lifecycleScope.launch(Dispatchers.IO) {
launch { launch {
if (tunnelConfig != null) { if (tunnelConfig != null) {
try { try {
@ -77,7 +72,8 @@ class WireGuardTunnelService : ForegroundService() {
val settings = settingsRepository.getSettings() val settings = settingsRepository.getSettings()
val tunnels = tunnelConfigRepository.getAll() val tunnels = tunnelConfigRepository.getAll()
if (settings.isAlwaysOnVpnEnabled) { if (settings.isAlwaysOnVpnEnabled) {
val tunnel = if(settings.defaultTunnel != null) { val tunnel =
if (settings.defaultTunnel != null) {
TunnelConfig.from(settings.defaultTunnel!!) TunnelConfig.from(settings.defaultTunnel!!)
} else if (tunnels.isNotEmpty()) { } else if (tunnels.isNotEmpty()) {
tunnels.first() tunnels.first()
@ -88,7 +84,6 @@ class WireGuardTunnelService : ForegroundService() {
tunnelName = tunnel.name tunnelName = tunnel.name
vpnService.startTunnel(tunnel) vpnService.startTunnel(tunnel)
} }
} }
} }
} }
@ -103,12 +98,16 @@ class WireGuardTunnelService : ForegroundService() {
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> { statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
if (!didShowConnected) { if (!didShowConnected) {
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY) delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
launchVpnNotification(getString(R.string.tunnel_start_title),"${getString(R.string.tunnel_start_text)} $tunnelName") launchVpnNotification(
getString(R.string.tunnel_start_title),
"${getString(R.string.tunnel_start_text)} $tunnelName",
)
didShowConnected = true didShowConnected = true
} }
} }
statuses?.any { it == HandshakeStatus.STALE } == true -> {} statuses?.any { it == HandshakeStatus.STALE } == true -> {}
statuses?.all { it == HandshakeStatus.NOT_STARTED } == true -> {} statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
true -> {}
else -> {} else -> {}
} }
} }
@ -127,7 +126,10 @@ class WireGuardTunnelService : ForegroundService() {
stopSelf() stopSelf()
} }
private fun launchVpnNotification(title : String = getString(R.string.vpn_starting),description : String = getString(R.string.attempt_connection)) { private fun launchVpnNotification(
title: String = getString(R.string.vpn_starting),
description: String = getString(R.string.attempt_connection)
) {
val notification = val notification =
notificationService.createNotification( notificationService.createNotification(
channelId = getString(R.string.vpn_channel_id), channelId = getString(R.string.vpn_channel_id),
@ -136,13 +138,13 @@ class WireGuardTunnelService : ForegroundService() {
onGoing = false, onGoing = false,
vibration = false, vibration = false,
showTimestamp = true, showTimestamp = true,
description = description description = description,
) )
ServiceCompat.startForeground( ServiceCompat.startForeground(
this, this,
foregroundId, foregroundId,
notification, notification,
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
) )
} }
@ -156,20 +158,20 @@ class WireGuardTunnelService : ForegroundService() {
this, this,
0, 0,
Intent(this, NotificationActionReceiver::class.java), Intent(this, NotificationActionReceiver::class.java),
PendingIntent.FLAG_IMMUTABLE PendingIntent.FLAG_IMMUTABLE,
), ),
actionText = getString(R.string.restart), actionText = getString(R.string.restart),
title = getString(R.string.vpn_connection_failed), title = getString(R.string.vpn_connection_failed),
onGoing = false, onGoing = false,
vibration = true, vibration = true,
showTimestamp = true, showTimestamp = true,
description = message description = message,
) )
ServiceCompat.startForeground( ServiceCompat.startForeground(
this, this,
foregroundId, foregroundId,
notification, notification,
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
) )
} }

View File

@ -24,13 +24,13 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
private val wifiManager = private val wifiManager =
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
override val networkStatus = override val networkStatus = callbackFlow {
callbackFlow {
val networkStatusCallback = val networkStatusCallback =
when (Build.VERSION.SDK_INT) { when (Build.VERSION.SDK_INT) {
in Build.VERSION_CODES.S..Int.MAX_VALUE -> { in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
object : ConnectivityManager.NetworkCallback( object :
FLAG_INCLUDE_LOCATION_INFO ConnectivityManager.NetworkCallback(
FLAG_INCLUDE_LOCATION_INFO,
) { ) {
override fun onAvailable(network: Network) { override fun onAvailable(network: Network) {
trySend(NetworkStatus.Available(network)) trySend(NetworkStatus.Available(network))
@ -47,13 +47,12 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
trySend( trySend(
NetworkStatus.CapabilitiesChanged( NetworkStatus.CapabilitiesChanged(
network, network,
networkCapabilities networkCapabilities,
) ),
) )
} }
} }
} }
else -> { else -> {
object : ConnectivityManager.NetworkCallback() { object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) { override fun onAvailable(network: Network) {
@ -71,8 +70,8 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
trySend( trySend(
NetworkStatus.CapabilitiesChanged( NetworkStatus.CapabilitiesChanged(
network, network,
networkCapabilities networkCapabilities,
) ),
) )
} }
} }
@ -86,9 +85,7 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
.build() .build()
connectivityManager.registerNetworkCallback(request, networkStatusCallback) connectivityManager.registerNetworkCallback(request, networkStatusCallback)
awaitClose { awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) }
connectivityManager.unregisterNetworkCallback(networkStatusCallback)
}
} }
override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? { override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? {
@ -119,18 +116,16 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
inline fun <Result> Flow<NetworkStatus>.map( inline fun <Result> Flow<NetworkStatus>.map(
crossinline onUnavailable: suspend (network: Network) -> Result, crossinline onUnavailable: suspend (network: Network) -> Result,
crossinline onAvailable: suspend (network: Network) -> Result, crossinline onAvailable: suspend (network: Network) -> Result,
crossinline onCapabilitiesChanged: suspend ( crossinline onCapabilitiesChanged:
network: Network, suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result
networkCapabilities: NetworkCapabilities ): Flow<Result> = map { status ->
) -> Result
): Flow<Result> =
map { status ->
when (status) { when (status) {
is NetworkStatus.Unavailable -> onUnavailable(status.network) is NetworkStatus.Unavailable -> onUnavailable(status.network)
is NetworkStatus.Available -> onAvailable(status.network) is NetworkStatus.Available -> onAvailable(status.network)
is NetworkStatus.CapabilitiesChanged -> onCapabilitiesChanged( is NetworkStatus.CapabilitiesChanged ->
onCapabilitiesChanged(
status.network, status.network,
status.networkCapabilities status.networkCapabilities,
) )
} }
} }

View File

@ -5,9 +5,5 @@ import android.net.NetworkCapabilities
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject import javax.inject.Inject
class EthernetService class EthernetService @Inject constructor(@ApplicationContext context: Context) :
@Inject
constructor(
@ApplicationContext context: Context
) :
BaseNetworkService<EthernetService>(context, NetworkCapabilities.TRANSPORT_ETHERNET) BaseNetworkService<EthernetService>(context, NetworkCapabilities.TRANSPORT_ETHERNET)

View File

@ -5,9 +5,5 @@ import android.net.NetworkCapabilities
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject import javax.inject.Inject
class MobileDataService class MobileDataService @Inject constructor(@ApplicationContext context: Context) :
@Inject
constructor(
@ApplicationContext context: Context
) :
BaseNetworkService<MobileDataService>(context, NetworkCapabilities.TRANSPORT_CELLULAR) BaseNetworkService<MobileDataService>(context, NetworkCapabilities.TRANSPORT_CELLULAR)

View File

@ -5,9 +5,5 @@ import android.net.NetworkCapabilities
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject import javax.inject.Inject
class WifiService class WifiService @Inject constructor(@ApplicationContext context: Context) :
@Inject
constructor(
@ApplicationContext context: Context
) :
BaseNetworkService<WifiService>(context, NetworkCapabilities.TRANSPORT_WIFI) BaseNetworkService<WifiService>(context, NetworkCapabilities.TRANSPORT_WIFI)

View File

@ -3,7 +3,6 @@ package com.zaneschepke.wireguardautotunnel.service.notification
import android.app.Notification import android.app.Notification
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import androidx.core.app.NotificationCompat
interface NotificationService { interface NotificationService {
fun createNotification( fun createNotification(

View File

@ -13,22 +13,20 @@ import com.zaneschepke.wireguardautotunnel.ui.MainActivity
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject import javax.inject.Inject
class WireGuardNotification class WireGuardNotification @Inject constructor(@ApplicationContext private val context: Context) :
@Inject NotificationService {
constructor(
@ApplicationContext private val context: Context
) : NotificationService {
private val notificationManager = private val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private val watcherBuilder: NotificationCompat.Builder = private val watcherBuilder: NotificationCompat.Builder =
NotificationCompat.Builder( NotificationCompat.Builder(
context, context,
context.getString(R.string.watcher_channel_id) context.getString(R.string.watcher_channel_id),
) )
private val tunnelBuilder: NotificationCompat.Builder = NotificationCompat.Builder( private val tunnelBuilder: NotificationCompat.Builder =
NotificationCompat.Builder(
context, context,
context.getString(R.string.vpn_channel_id) context.getString(R.string.vpn_channel_id),
) )
override fun createNotification( override fun createNotification(
@ -49,8 +47,9 @@ constructor(
NotificationChannel( NotificationChannel(
channelId, channelId,
channelName, channelName,
importance importance,
).let { )
.let {
it.description = title it.description = title
it.enableLights(lights) it.enableLights(lights)
it.lightColor = Color.RED it.lightColor = Color.RED
@ -65,17 +64,18 @@ constructor(
context, context,
0, 0,
notificationIntent, notificationIntent,
PendingIntent.FLAG_IMMUTABLE PendingIntent.FLAG_IMMUTABLE,
) )
} }
val builder = when(channelId) { val builder =
when (channelId) {
context.getString(R.string.watcher_channel_id) -> watcherBuilder context.getString(R.string.watcher_channel_id) -> watcherBuilder
context.getString(R.string.vpn_channel_id) -> tunnelBuilder context.getString(R.string.vpn_channel_id) -> tunnelBuilder
else -> { else -> {
NotificationCompat.Builder( NotificationCompat.Builder(
context, context,
channelId channelId,
) )
} }
} }
@ -83,8 +83,7 @@ constructor(
return builder.let { return builder.let {
if (action != null && actionText != null) { if (action != null && actionText != null) {
it.addAction( it.addAction(
NotificationCompat.Action.Builder(0, actionText, action) NotificationCompat.Action.Builder(0, actionText, action).build(),
.build()
) )
it.setAutoCancel(true) it.setAutoCancel(true)
} }

View File

@ -12,33 +12,34 @@ import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class ShortcutsActivity : ComponentActivity() { class ShortcutsActivity : ComponentActivity() {
@Inject @Inject lateinit var settingsRepository: SettingsRepository
lateinit var settingsRepository: SettingsRepository
@Inject @Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
lateinit var tunnelConfigRepository: TunnelConfigRepository
private suspend fun toggleWatcherServicePause() { private suspend fun toggleWatcherServicePause() {
val settings = settingsRepository.getSettings() val settings = settingsRepository.getSettings()
if (settings.isAutoTunnelEnabled) { if (settings.isAutoTunnelEnabled) {
val pauseAutoTunnel = !settings.isAutoTunnelPaused val pauseAutoTunnel = !settings.isAutoTunnelPaused
settingsRepository.save(settings.copy( settingsRepository.save(
isAutoTunnelPaused = pauseAutoTunnel settings.copy(
)) isAutoTunnelPaused = pauseAutoTunnel,
),
)
} }
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(View(this)) setContentView(View(this))
if (intent.getStringExtra(CLASS_NAME_EXTRA_KEY) if (
intent
.getStringExtra(CLASS_NAME_EXTRA_KEY)
.equals(WireGuardTunnelService::class.java.simpleName) .equals(WireGuardTunnelService::class.java.simpleName)
) { ) {
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
@ -48,7 +49,9 @@ class ShortcutsActivity : ComponentActivity() {
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY) val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
val tunnelConfig = val tunnelConfig =
if (tunnelName != null) { if (tunnelName != null) {
tunnelConfigRepository.getAll().firstOrNull { it.name == tunnelName } tunnelConfigRepository.getAll().firstOrNull {
it.name == tunnelName
}
} else { } else {
if (settings.defaultTunnel == null) { if (settings.defaultTunnel == null) {
tunnelConfigRepository.getAll().first() tunnelConfigRepository.getAll().first()
@ -59,12 +62,14 @@ class ShortcutsActivity : ComponentActivity() {
tunnelConfig ?: return@launch tunnelConfig ?: return@launch
toggleWatcherServicePause() toggleWatcherServicePause()
when (intent.action) { when (intent.action) {
Action.STOP.name -> ServiceManager.stopVpnService( Action.STOP.name ->
this@ShortcutsActivity ServiceManager.stopVpnService(
)
Action.START.name -> ServiceManager.startVpnServiceForeground(
this@ShortcutsActivity, this@ShortcutsActivity,
tunnelConfig.toString() )
Action.START.name ->
ServiceManager.startVpnServiceForeground(
this@ShortcutsActivity,
tunnelConfig.toString(),
) )
} }
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -20,14 +20,11 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class TunnelControlTile() : TileService() { class TunnelControlTile() : TileService() {
@Inject @Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
lateinit var tunnelConfigRepository: TunnelConfigRepository
@Inject @Inject lateinit var settingsRepository: SettingsRepository
lateinit var settingsRepository: SettingsRepository
@Inject @Inject lateinit var vpnService: VpnService
lateinit var vpnService: VpnService
private val scope = CoroutineScope(Dispatchers.IO) private val scope = CoroutineScope(Dispatchers.IO)
@ -48,7 +45,8 @@ class TunnelControlTile() : TileService() {
setUnavailable() setUnavailable()
return@collect return@collect
} }
tunnelName = it.name.ifBlank { tunnelName =
it.name.ifBlank {
val settings = settingsRepository.getSettings() val settings = settingsRepository.getSettings()
if (settings.defaultTunnel != null) { if (settings.defaultTunnel != null) {
TunnelConfig.from(settings.defaultTunnel!!).name TunnelConfig.from(settings.defaultTunnel!!).name
@ -58,6 +56,7 @@ class TunnelControlTile() : TileService() {
} }
} }
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
scope.cancel() scope.cancel()
@ -73,14 +72,15 @@ class TunnelControlTile() : TileService() {
unlockAndRun { unlockAndRun {
scope.launch { scope.launch {
try { try {
val tunnelConfig = tunnelConfigRepository.getAll().first { it.name == tunnelName } val tunnelConfig =
tunnelConfigRepository.getAll().first { it.name == tunnelName }
toggleWatcherServicePause() toggleWatcherServicePause()
if (vpnService.getState() == Tunnel.State.UP) { if (vpnService.getState() == Tunnel.State.UP) {
ServiceManager.stopVpnService(this@TunnelControlTile) ServiceManager.stopVpnService(this@TunnelControlTile)
} else { } else {
ServiceManager.startVpnServiceForeground( ServiceManager.startVpnServiceForeground(
this@TunnelControlTile, this@TunnelControlTile,
tunnelConfig.toString() tunnelConfig.toString(),
) )
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -97,9 +97,11 @@ class TunnelControlTile() : TileService() {
val settings = settingsRepository.getSettings() val settings = settingsRepository.getSettings()
if (settings.isAutoTunnelEnabled) { if (settings.isAutoTunnelEnabled) {
val pauseAutoTunnel = !settings.isAutoTunnelPaused val pauseAutoTunnel = !settings.isAutoTunnelPaused
settingsRepository.save(settings.copy( settingsRepository.save(
isAutoTunnelPaused = pauseAutoTunnel settings.copy(
)) isAutoTunnelPaused = pauseAutoTunnel,
),
)
} }
} }
} }

View File

@ -4,13 +4,13 @@ enum class HandshakeStatus {
HEALTHY, HEALTHY,
STALE, STALE,
UNKNOWN, UNKNOWN,
NOT_STARTED NOT_STARTED;
;
companion object { companion object {
private const val WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC = 180 private const val WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC = 180
const val STATUS_CHANGE_TIME_BUFFER = 30 const val STATUS_CHANGE_TIME_BUFFER = 30
const val STALE_TIME_LIMIT_SEC = WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC + STATUS_CHANGE_TIME_BUFFER const val STALE_TIME_LIMIT_SEC =
WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC + STATUS_CHANGE_TIME_BUFFER
const val NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC = 30 const val NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC = 30
} }
} }

View File

@ -67,7 +67,7 @@ constructor(
backend.setState( backend.setState(
this, this,
State.UP, State.UP,
config config,
) )
emitTunnelState(state) emitTunnelState(state)
state state
@ -80,24 +80,24 @@ constructor(
private fun emitTunnelState(state: State) { private fun emitTunnelState(state: State) {
_vpnState.tryEmit( _vpnState.tryEmit(
_vpnState.value.copy( _vpnState.value.copy(
status = state status = state,
) ),
) )
} }
private fun emitBackendStatistics(statistics: Statistics) { private fun emitBackendStatistics(statistics: Statistics) {
_vpnState.tryEmit( _vpnState.tryEmit(
_vpnState.value.copy( _vpnState.value.copy(
statistics = statistics statistics = statistics,
) ),
) )
} }
private suspend fun emitTunnelName(name: String) { private suspend fun emitTunnelName(name: String) {
_vpnState.emit( _vpnState.emit(
_vpnState.value.copy( _vpnState.value.copy(
name = name name = name,
) ),
) )
} }

View File

@ -6,8 +6,8 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class ActivityViewModel @Inject constructor( class ActivityViewModel
@Inject
constructor(
private val settingsRepo: SettingsDao, private val settingsRepo: SettingsDao,
) : ViewModel() { ) : ViewModel() {}
}

View File

@ -28,6 +28,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
@ -37,6 +38,9 @@ import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
import com.wireguard.android.backend.GoBackend import com.wireguard.android.backend.GoBackend
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
@ -50,14 +54,35 @@ import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.IOException
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@Inject
lateinit var dataStoreManager: DataStoreManager
@Inject lateinit var settingsRepository: SettingsRepository
@OptIn( @OptIn(
ExperimentalPermissionsApi::class ExperimentalPermissionsApi::class,
) )
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// load preferences into memory and init data
lifecycleScope.launch {
try {
dataStoreManager.init()
if (settingsRepository.getAll().isEmpty()) {
settingsRepository.save(com.zaneschepke.wireguardautotunnel.data.model.Settings())
}
WireGuardAutoTunnel.requestTileServiceStateUpdate()
} catch (e: IOException) {
Timber.e("Failed to load preferences")
}
}
setContent { setContent {
// val activityViewModel = hiltViewModel<ActivityViewModel>() // val activityViewModel = hiltViewModel<ActivityViewModel>()
@ -73,7 +98,10 @@ class MainActivity : AppCompatActivity() {
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
fun requestNotificationPermission() { fun requestNotificationPermission() {
if (!notificationPermissionState.status.isGranted && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (
!notificationPermissionState.status.isGranted &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
) {
notificationPermissionState.launchPermissionRequest() notificationPermissionState.launchPermissionRequest()
} }
} }
@ -87,7 +115,7 @@ class MainActivity : AppCompatActivity() {
if (accepted) { if (accepted) {
vpnIntent = null vpnIntent = null
} }
} },
) )
LaunchedEffect(vpnIntent) { LaunchedEffect(vpnIntent) {
if (vpnIntent != null) { if (vpnIntent != null) {
@ -99,13 +127,15 @@ class MainActivity : AppCompatActivity() {
fun showSnackBarMessage(message: String) { fun showSnackBarMessage(message: String) {
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
val result = snackbarHostState.showSnackbar( val result =
snackbarHostState.showSnackbar(
message = message, message = message,
actionLabel = applicationContext.getString(R.string.okay), actionLabel = applicationContext.getString(R.string.okay),
duration = SnackbarDuration.Short duration = SnackbarDuration.Short,
) )
when (result) { when (result) {
SnackbarResult.ActionPerformed, SnackbarResult.Dismissed -> { SnackbarResult.ActionPerformed,
SnackbarResult.Dismissed -> {
snackbarHostState.currentSnackbarData?.dismiss() snackbarHostState.currentSnackbarData?.dismiss()
} }
} }
@ -118,29 +148,36 @@ class MainActivity : AppCompatActivity() {
CustomSnackBar( CustomSnackBar(
snackbarData.visuals.message, snackbarData.visuals.message,
isRtl = false, isRtl = false,
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( containerColor =
2.dp MaterialTheme.colorScheme.surfaceColorAtElevation(
) 2.dp,
),
) )
} }
}, },
modifier = Modifier.focusable().focusProperties { up = focusRequester }, modifier = Modifier.focusable().focusProperties { up = focusRequester },
bottomBar = bottomBar =
if (vpnIntent == null && notificationPermissionState.status.isGranted) { if (vpnIntent == null && notificationPermissionState.status.isGranted) {
{ BottomNavBar(navController, listOf( {
BottomNavBar(
navController,
listOf(
Screen.Main.navItem, Screen.Main.navItem,
Screen.Settings.navItem, Screen.Settings.navItem,
Screen.Support.navItem)) } Screen.Support.navItem,
),
)
}
} else { } else {
{} {}
} },
) { padding -> ) { padding ->
if (vpnIntent != null) { if (vpnIntent != null) {
PermissionRequestFailedScreen( PermissionRequestFailedScreen(
padding = padding, padding = padding,
onRequestAgain = { vpnActivityResultState.launch(vpnIntent) }, onRequestAgain = { vpnActivityResultState.launch(vpnIntent) },
message = getString(R.string.vpn_permission_required), message = getString(R.string.vpn_permission_required),
getString(R.string.retry) getString(R.string.retry),
) )
return@Scaffold return@Scaffold
} }
@ -154,12 +191,12 @@ class MainActivity : AppCompatActivity() {
Uri.fromParts( Uri.fromParts(
Constants.URI_PACKAGE_SCHEME, Constants.URI_PACKAGE_SCHEME,
this.packageName, this.packageName,
null null,
) )
startActivity(intentSettings) startActivity(intentSettings)
}, },
message = getString(R.string.notification_permission_required), message = getString(R.string.notification_permission_required),
getString(R.string.open_settings) getString(R.string.open_settings),
) )
return@Scaffold return@Scaffold
} }
@ -167,22 +204,30 @@ class MainActivity : AppCompatActivity() {
composable( composable(
Screen.Main.route, Screen.Main.route,
) { ) {
MainScreen(padding = padding, focusRequester = focusRequester, showSnackbarMessage = { message -> MainScreen(
showSnackBarMessage(message) padding = padding,
}, navController = navController) focusRequester = focusRequester,
showSnackbarMessage = { message -> showSnackBarMessage(message) },
navController = navController,
)
} }
composable(Screen.Settings.route, composable(
Screen.Settings.route,
) { ) {
SettingsScreen(padding = padding, showSnackbarMessage = { message -> SettingsScreen(
showSnackBarMessage(message) padding = padding,
}, focusRequester = focusRequester) showSnackbarMessage = { message -> showSnackBarMessage(message) },
focusRequester = focusRequester,
)
} }
composable(Screen.Support.route, composable(
Screen.Support.route,
) { ) {
SupportScreen(padding = padding, focusRequester = focusRequester, SupportScreen(
showSnackbarMessage = { message -> padding = padding,
showSnackBarMessage(message) focusRequester = focusRequester,
}) showSnackbarMessage = { message -> showSnackBarMessage(message) },
)
} }
composable("${Screen.Config.route}/{id}") { composable("${Screen.Config.route}/{id}") {
val id = it.arguments?.getString("id") val id = it.arguments?.getString("id")
@ -194,7 +239,7 @@ class MainActivity : AppCompatActivity() {
showSnackbarMessage = { message -> showSnackbarMessage = { message ->
showSnackBarMessage(message) showSnackBarMessage(message)
}, },
focusRequester = focusRequester focusRequester = focusRequester,
) )
} }
} }

View File

@ -8,26 +8,31 @@ import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
sealed class Screen(val route: String) { sealed class Screen(val route: String) {
data object Main : Screen("main") { data object Main : Screen("main") {
val navItem = BottomNavItem( val navItem =
BottomNavItem(
name = "Tunnels", name = "Tunnels",
route = route, route = route,
icon = Icons.Rounded.Home icon = Icons.Rounded.Home,
) )
} }
data object Settings : Screen("settings") { data object Settings : Screen("settings") {
val navItem = BottomNavItem( val navItem =
BottomNavItem(
name = "Settings", name = "Settings",
route = route, route = route,
icon = Icons.Rounded.Settings icon = Icons.Rounded.Settings,
) )
} }
data object Support : Screen("support") { data object Support : Screen("support") {
val navItem = BottomNavItem( val navItem =
BottomNavItem(
name = "Support", name = "Support",
route = route, route = route,
icon = Icons.Rounded.QuestionMark icon = Icons.Rounded.QuestionMark,
) )
} }
data object Config : Screen("config")
data object Config : Screen("config")
} }

View File

@ -23,7 +23,7 @@ fun ClickableIconButton(
) { ) {
TextButton( TextButton(
onClick = onClick, onClick = onClick,
enabled = enabled enabled = enabled,
) { ) {
Text(text, Modifier.weight(1f, false)) Text(text, Modifier.weight(1f, false))
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing)) Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
@ -35,7 +35,7 @@ fun ClickableIconButton(
if (enabled) { if (enabled) {
onIconClick() onIconClick()
} }
} },
) )
} }
} }

View File

@ -26,17 +26,12 @@ fun PermissionRequestFailedScreen(
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
modifier = modifier = Modifier.fillMaxSize().padding(padding),
Modifier
.fillMaxSize()
.padding(padding)
) { ) {
Text(message, textAlign = TextAlign.Center, modifier = Modifier.padding(15.dp)) Text(message, textAlign = TextAlign.Center, modifier = Modifier.padding(15.dp))
Button(onClick = { Button(
scope.launch { onClick = { scope.launch { onRequestAgain() } },
onRequestAgain() ) {
}
}) {
Text(buttonText) Text(buttonText)
} }
} }

View File

@ -34,30 +34,22 @@ fun RowListItem(
) { ) {
Box( Box(
modifier = modifier =
Modifier Modifier.animateContentSize()
.animateContentSize()
.clip(RoundedCornerShape(30.dp)) .clip(RoundedCornerShape(30.dp))
.combinedClickable( .combinedClickable(
onClick = { onClick = { onClick() },
onClick() onLongClick = { onHold() },
}, ),
onLongClick = {
onHold()
}
)
) { ) {
Column { Column {
Row( Row(
modifier = modifier = Modifier.fillMaxWidth().padding(horizontal = 15.dp, vertical = 5.dp),
Modifier
.fillMaxWidth()
.padding(horizontal = 15.dp, vertical = 5.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(.60f) modifier = Modifier.fillMaxWidth(.60f),
) { ) {
icon() icon()
Text(text) Text(text)
@ -68,11 +60,10 @@ fun RowListItem(
statistics?.peers()?.forEach { statistics?.peers()?.forEach {
Row( Row(
modifier = modifier =
Modifier Modifier.fillMaxWidth()
.fillMaxWidth()
.padding(end = 10.dp, bottom = 10.dp, start = 10.dp), .padding(end = 10.dp, bottom = 10.dp, start = 10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly horizontalArrangement = Arrangement.SpaceEvenly,
) { ) {
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
val peerTx = statistics.peer(it)!!.txBytes val peerTx = statistics.peer(it)!!.txBytes

View File

@ -47,7 +47,7 @@ fun SearchBar(onQuery: (queryString: String) -> Unit) {
Icon( Icon(
imageVector = Icons.Rounded.Search, imageVector = Icons.Rounded.Search,
tint = MaterialTheme.colorScheme.onBackground, tint = MaterialTheme.colorScheme.onBackground,
contentDescription = stringResource(id = R.string.search_icon) contentDescription = stringResource(id = R.string.search_icon),
) )
}, },
trailingIcon = { trailingIcon = {
@ -56,7 +56,7 @@ fun SearchBar(onQuery: (queryString: String) -> Unit) {
Icon( Icon(
imageVector = Icons.Rounded.Clear, imageVector = Icons.Rounded.Clear,
tint = MaterialTheme.colorScheme.onBackground, tint = MaterialTheme.colorScheme.onBackground,
contentDescription = stringResource(id = R.string.clear_icon) contentDescription = stringResource(id = R.string.clear_icon),
) )
} }
} }
@ -66,15 +66,14 @@ fun SearchBar(onQuery: (queryString: String) -> Unit) {
TextFieldDefaults.colors( TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent, focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent, unfocusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent disabledContainerColor = Color.Transparent,
), ),
placeholder = { Text(text = stringResource(R.string.hint_search_packages)) }, placeholder = { Text(text = stringResource(R.string.hint_search_packages)) },
textStyle = MaterialTheme.typography.bodySmall, textStyle = MaterialTheme.typography.bodySmall,
singleLine = true, singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
modifier = modifier =
Modifier Modifier.fillMaxWidth()
.fillMaxWidth() .background(color = MaterialTheme.colorScheme.background, shape = RectangleShape),
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape)
) )
} }

View File

@ -22,19 +22,15 @@ fun ConfigurationTextBox(
modifier = modifier, modifier = modifier,
value = value, value = value,
singleLine = true, singleLine = true,
onValueChange = { onValueChange = { onValueChange(it) },
onValueChange(it)
},
label = { Text(label) }, label = { Text(label) },
maxLines = 1, maxLines = 1,
placeholder = { placeholder = { Text(hint) },
Text(hint)
},
keyboardOptions = keyboardOptions =
KeyboardOptions( KeyboardOptions(
capitalization = KeyboardCapitalization.None, capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done imeAction = ImeAction.Done,
), ),
keyboardActions = keyboardActions keyboardActions = keyboardActions,
) )
} }

View File

@ -21,21 +21,16 @@ fun ConfigurationToggle(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Row( Row(
modifier = modifier = Modifier.fillMaxWidth().padding(padding),
Modifier
.fillMaxWidth()
.padding(padding),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
Text(label) Text(label)
Switch( Switch(
modifier = modifier, modifier = modifier,
enabled = enabled, enabled = enabled,
checked = checked, checked = checked,
onCheckedChange = { onCheckedChange = { onCheckChanged() },
onCheckChanged()
}
) )
} }
} }

View File

@ -11,14 +11,11 @@ import androidx.navigation.NavController
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
@Composable @Composable
fun BottomNavBar( fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) {
navController: NavController,
bottomNavItems: List<BottomNavItem>
) {
val backStackEntry = navController.currentBackStackEntryAsState() val backStackEntry = navController.currentBackStackEntryAsState()
NavigationBar( NavigationBar(
containerColor = MaterialTheme.colorScheme.background containerColor = MaterialTheme.colorScheme.background,
) { ) {
bottomNavItems.forEach { item -> bottomNavItems.forEach { item ->
val selected = item.route == backStackEntry.value?.destination?.route val selected = item.route == backStackEntry.value?.destination?.route
@ -29,15 +26,15 @@ fun BottomNavBar(
label = { label = {
Text( Text(
text = item.name, text = item.name,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold,
) )
}, },
icon = { icon = {
Icon( Icon(
imageVector = item.icon, imageVector = item.icon,
contentDescription = "${item.name} Icon" contentDescription = "${item.name} Icon",
) )
} },
) )
} }
} }

View File

@ -11,47 +11,36 @@ import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
@Composable @Composable
fun AuthorizationPrompt( fun AuthorizationPrompt(onSuccess: () -> Unit, onFailure: () -> Unit, onError: (String) -> Unit) {
onSuccess: () -> Unit,
onFailure: () -> Unit,
onError: (String) -> Unit
) {
val context = LocalContext.current val context = LocalContext.current
val biometricManager = BiometricManager.from(context) val biometricManager = BiometricManager.from(context)
val bio = biometricManager.canAuthenticate(BIOMETRIC_WEAK or DEVICE_CREDENTIAL) val bio = biometricManager.canAuthenticate(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
val isBiometricAvailable = val isBiometricAvailable = remember {
remember {
when (bio) { when (bio) {
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> { BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
onError("Biometrics not available") onError("Biometrics not available")
false false
} }
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> { BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
onError("Biometrics not created") onError("Biometrics not created")
false false
} }
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> { BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
onError("Biometric hardware not found") onError("Biometric hardware not found")
false false
} }
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> { BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
onError("Biometric security update required") onError("Biometric security update required")
false false
} }
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> { BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
onError("Biometrics not supported") onError("Biometrics not supported")
false false
} }
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> { BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
onError("Biometrics status unknown") onError("Biometrics status unknown")
false false
} }
BiometricManager.BIOMETRIC_SUCCESS -> true BiometricManager.BIOMETRIC_SUCCESS -> true
else -> false else -> false
} }
@ -71,10 +60,7 @@ fun AuthorizationPrompt(
context as FragmentActivity, context as FragmentActivity,
executor, executor,
object : BiometricPrompt.AuthenticationCallback() { object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError( override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
errorCode: Int,
errString: CharSequence
) {
super.onAuthenticationError(errorCode, errString) super.onAuthenticationError(errorCode, errString)
onFailure() onFailure()
} }
@ -90,7 +76,7 @@ fun AuthorizationPrompt(
super.onAuthenticationFailed() super.onAuthenticationFailed()
onFailure() onFailure()
} }
} },
) )
biometricPrompt.authenticate(promptInfo) biometricPrompt.authenticate(promptInfo)
} }

View File

@ -38,24 +38,24 @@ fun CustomSnackBar(
containerColor = containerColor, containerColor = containerColor,
modifier = modifier =
Modifier.fillMaxWidth( Modifier.fillMaxWidth(
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f,
).padding(bottom = 100.dp), )
shape = RoundedCornerShape(16.dp) .padding(bottom = 100.dp),
shape = RoundedCornerShape(16.dp),
) { ) {
CompositionLocalProvider( CompositionLocalProvider(
LocalLayoutDirection provides LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr,
if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
) { ) {
Row( Row(
modifier = Modifier.width(IntrinsicSize.Max).height(IntrinsicSize.Min), modifier = Modifier.width(IntrinsicSize.Max).height(IntrinsicSize.Min),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start,
) { ) {
Icon( Icon(
Icons.Rounded.Info, Icons.Rounded.Info,
contentDescription = stringResource(R.string.info), contentDescription = stringResource(R.string.info),
tint = Color.White, tint = Color.White,
modifier = Modifier.padding(end = 10.dp) modifier = Modifier.padding(end = 10.dp),
) )
Text(message, color = Color.White, modifier = Modifier.padding(end = 5.dp)) Text(message, color = Color.White, modifier = Modifier.padding(end = 5.dp))
} }

View File

@ -16,7 +16,8 @@ fun LoadingScreen() {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = Modifier.fillMaxSize().focusable().padding()) { modifier = Modifier.fillMaxSize().focusable().padding(),
) {
Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() } Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() }
} }
} }

View File

@ -12,14 +12,11 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@Composable @Composable
fun SectionTitle( fun SectionTitle(title: String, padding: Dp) {
title: String,
padding: Dp
) {
Text( Text(
title, title,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.ExtraBold), style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.ExtraBold),
modifier = Modifier.padding(padding, bottom = 5.dp, top = 5.dp) modifier = Modifier.padding(padding, bottom = 5.dp, top = 5.dp),
) )
} }

View File

@ -17,13 +17,13 @@ data class InterfaceProxy(
privateKey = i.keyPair.privateKey.toBase64().trim(), privateKey = i.keyPair.privateKey.toBase64().trim(),
addresses = i.addresses.joinToString(", ").trim(), addresses = i.addresses.joinToString(", ").trim(),
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(), dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
listenPort = if (i.listenPort.isPresent) { listenPort =
i.listenPort.get().toString() if (i.listenPort.isPresent) {
.trim() i.listenPort.get().toString().trim()
} else { } else {
"" ""
}, },
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "" mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "",
) )
} }
} }

View File

@ -13,36 +13,60 @@ data class PeerProxy(
fun from(peer: Peer): PeerProxy { fun from(peer: Peer): PeerProxy {
return PeerProxy( return PeerProxy(
publicKey = peer.publicKey.toBase64(), publicKey = peer.publicKey.toBase64(),
preSharedKey = if (peer.preSharedKey.isPresent) { preSharedKey =
peer.preSharedKey.get().toBase64() if (peer.preSharedKey.isPresent) {
.trim() peer.preSharedKey.get().toBase64().trim()
} else { } else {
"" ""
}, },
persistentKeepalive = if (peer.persistentKeepalive.isPresent) { persistentKeepalive =
peer.persistentKeepalive.get() if (peer.persistentKeepalive.isPresent) {
.toString().trim() peer.persistentKeepalive.get().toString().trim()
} else { } else {
"" ""
}, },
endpoint = if (peer.endpoint.isPresent) { endpoint =
peer.endpoint.get().toString() if (peer.endpoint.isPresent) {
.trim() peer.endpoint.get().toString().trim()
} else { } else {
"" ""
}, },
allowedIps = peer.allowedIps.joinToString(", ").trim() allowedIps = peer.allowedIps.joinToString(", ").trim(),
) )
} }
val IPV4_PUBLIC_NETWORKS = val IPV4_PUBLIC_NETWORKS =
setOf( setOf(
"0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3", "0.0.0.0/5",
"64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12", "8.0.0.0/7",
"172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7", "11.0.0.0/8",
"176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16", "12.0.0.0/6",
"192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10", "16.0.0.0/4",
"193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4" "32.0.0.0/3",
"64.0.0.0/2",
"128.0.0.0/3",
"160.0.0.0/5",
"168.0.0.0/6",
"172.0.0.0/12",
"172.32.0.0/11",
"172.64.0.0/10",
"172.128.0.0/9",
"173.0.0.0/8",
"174.0.0.0/7",
"176.0.0.0/4",
"192.0.0.0/9",
"192.128.0.0/11",
"192.160.0.0/13",
"192.169.0.0/16",
"192.170.0.0/15",
"192.172.0.0/14",
"192.176.0.0/12",
"192.192.0.0/10",
"193.0.0.0/8",
"194.0.0.0/7",
"196.0.0.0/6",
"200.0.0.0/5",
"208.0.0.0/4",
) )
val IPV4_WILDCARD = setOf("0.0.0.0/0") val IPV4_WILDCARD = setOf("0.0.0.0/0")
} }

View File

@ -88,7 +88,8 @@ import kotlinx.coroutines.delay
@OptIn( @OptIn(
ExperimentalComposeUiApi::class, ExperimentalComposeUiApi::class,
ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class,
ExperimentalFoundationApi::class) ExperimentalFoundationApi::class,
)
@Composable @Composable
fun ConfigScreen( fun ConfigScreen(
viewModel: ConfigViewModel = hiltViewModel(), viewModel: ConfigViewModel = hiltViewModel(),
@ -106,9 +107,7 @@ fun ConfigScreen(
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
LaunchedEffect(Unit) { LaunchedEffect(Unit) { viewModel.init(id) }
viewModel.init(id)
}
LaunchedEffect(uiState.loading) { LaunchedEffect(uiState.loading) {
if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) { if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
@ -124,8 +123,7 @@ fun ConfigScreen(
val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }) val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() })
val keyboardOptions = val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done)
KeyboardOptions(imeAction = ImeAction.Done)
val fillMaxHeight = .85f val fillMaxHeight = .85f
val fillMaxWidth = .85f val fillMaxWidth = .85f
@ -136,7 +134,8 @@ fun ConfigScreen(
if (uiState.isAllApplicationsEnabled) { if (uiState.isAllApplicationsEnabled) {
"all" "all"
} else { } else {
"${uiState.checkedPackageNames.size} " + (if (uiState.include) "included" else "excluded") "${uiState.checkedPackageNames.size} " +
(if (uiState.include) "included" else "excluded")
} }
} }
@ -153,12 +152,15 @@ fun ConfigScreen(
onFailure = { onFailure = {
showAuthPrompt = false showAuthPrompt = false
showSnackbarMessage(Event.Error.AuthorizationFailed.message) showSnackbarMessage(Event.Error.AuthorizationFailed.message)
}) },
)
} }
if (showApplicationsDialog) { if (showApplicationsDialog) {
val sortedPackages = val sortedPackages =
remember(uiState.packages) { uiState.packages.sortedBy { viewModel.getPackageLabel(it) } } remember(uiState.packages) {
uiState.packages.sortedBy { viewModel.getPackageLabel(it) }
}
AlertDialog(onDismissRequest = { showApplicationsDialog = false }) { AlertDialog(onDismissRequest = { showApplicationsDialog = false }) {
Surface( Surface(
tonalElevation = 2.dp, tonalElevation = 2.dp,
@ -167,55 +169,75 @@ fun ConfigScreen(
color = MaterialTheme.colorScheme.surface, color = MaterialTheme.colorScheme.surface,
modifier = modifier =
Modifier.fillMaxWidth() Modifier.fillMaxWidth()
.fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f)) { .fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f),
) {
Column(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.fillMaxWidth()) {
Row( Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp), modifier =
Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween) { horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(stringResource(id = R.string.tunnel_all)) Text(stringResource(id = R.string.tunnel_all))
Switch( Switch(
checked = uiState.isAllApplicationsEnabled, checked = uiState.isAllApplicationsEnabled,
onCheckedChange = { viewModel.onAllApplicationsChange(it) }) onCheckedChange = { viewModel.onAllApplicationsChange(it) },
)
} }
if (!uiState.isAllApplicationsEnabled) { if (!uiState.isAllApplicationsEnabled) {
Row( Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp), modifier =
Modifier.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween) { horizontalArrangement = Arrangement.SpaceBetween,
) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween) { horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(stringResource(id = R.string.include)) Text(stringResource(id = R.string.include))
Checkbox( Checkbox(
checked = uiState.include, checked = uiState.include,
onCheckedChange = { viewModel.onIncludeChange(!uiState.include) }) onCheckedChange = {
viewModel.onIncludeChange(!uiState.include)
},
)
} }
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween) { horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(stringResource(id = R.string.exclude)) Text(stringResource(id = R.string.exclude))
Checkbox( Checkbox(
checked = !uiState.include, checked = !uiState.include,
onCheckedChange = { viewModel.onIncludeChange(!uiState.include) }) onCheckedChange = {
viewModel.onIncludeChange(!uiState.include)
},
)
} }
} }
Row( Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp), modifier =
Modifier.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween) { horizontalArrangement = Arrangement.SpaceBetween,
) {
SearchBar(viewModel::emitQueriedPackages) SearchBar(viewModel::emitQueriedPackages)
} }
Spacer(Modifier.padding(5.dp)) Spacer(Modifier.padding(5.dp))
LazyColumn( LazyColumn(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = Modifier.fillMaxHeight(4 / 5f)) { modifier = Modifier.fillMaxHeight(4 / 5f),
) {
items(sortedPackages, key = { it.packageName }) { pack -> items(sortedPackages, key = { it.packageName }) { pack ->
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxSize().padding(5.dp)) { modifier = Modifier.fillMaxSize().padding(5.dp),
) {
Row(modifier = Modifier.fillMaxWidth(fillMaxWidth)) { Row(modifier = Modifier.fillMaxWidth(fillMaxWidth)) {
val drawable = val drawable =
pack.applicationInfo?.loadIcon(context.packageManager) pack.applicationInfo?.loadIcon(context.packageManager)
@ -223,28 +245,34 @@ fun ConfigScreen(
Image( Image(
painter = DrawablePainter(drawable), painter = DrawablePainter(drawable),
stringResource(id = R.string.icon), stringResource(id = R.string.icon),
modifier = Modifier.size(50.dp, 50.dp)) modifier = Modifier.size(50.dp, 50.dp),
)
} else { } else {
Icon( Icon(
Icons.Rounded.Android, Icons.Rounded.Android,
stringResource(id = R.string.edit), stringResource(id = R.string.edit),
modifier = Modifier.size(50.dp, 50.dp)) modifier = Modifier.size(50.dp, 50.dp),
)
} }
Text( Text(
viewModel.getPackageLabel(pack), viewModel.getPackageLabel(pack),
modifier = Modifier.padding(5.dp)) modifier = Modifier.padding(5.dp),
)
} }
Checkbox( Checkbox(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
checked = checked =
(uiState.checkedPackageNames.contains(pack.packageName)), (uiState.checkedPackageNames.contains(
pack.packageName
)),
onCheckedChange = { onCheckedChange = {
if (it) { if (it) {
viewModel.onAddCheckedPackage(pack.packageName) viewModel.onAddCheckedPackage(pack.packageName)
} else { } else {
viewModel.onRemoveCheckedPackage(pack.packageName) viewModel.onRemoveCheckedPackage(pack.packageName)
} }
}) },
)
} }
} }
} }
@ -252,7 +280,8 @@ fun ConfigScreen(
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize().padding(top = 5.dp), modifier = Modifier.fillMaxSize().padding(top = 5.dp),
horizontalArrangement = Arrangement.Center) { horizontalArrangement = Arrangement.Center,
) {
TextButton(onClick = { showApplicationsDialog = false }) { TextButton(onClick = { showApplicationsDialog = false }) {
Text(stringResource(R.string.done)) Text(stringResource(R.string.done))
} }
@ -287,19 +316,23 @@ fun ConfigScreen(
} }
}, },
containerColor = fobColor, containerColor = fobColor,
shape = RoundedCornerShape(16.dp)) { shape = RoundedCornerShape(16.dp),
) {
Icon( Icon(
imageVector = Icons.Rounded.Save, imageVector = Icons.Rounded.Save,
contentDescription = stringResource(id = R.string.save_changes), contentDescription = stringResource(id = R.string.save_changes),
tint = Color.DarkGray) tint = Color.DarkGray,
)
} }
}) { },
) {
Column { Column {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = modifier =
Modifier.verticalScroll(rememberScrollState()).weight(1f, true).fillMaxSize()) { Modifier.verticalScroll(rememberScrollState()).weight(1f, true).fillMaxSize(),
) {
Surface( Surface(
tonalElevation = 2.dp, tonalElevation = 2.dp,
shadowElevation = 2.dp, shadowElevation = 2.dp,
@ -311,57 +344,55 @@ fun ConfigScreen(
} else { } else {
Modifier.fillMaxWidth(fillMaxWidth) Modifier.fillMaxWidth(fillMaxWidth)
}) })
.padding(top = 50.dp, bottom = 10.dp)) { .padding(top = 50.dp, bottom = 10.dp),
) {
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp).focusGroup()) { modifier = Modifier.padding(15.dp).focusGroup(),
) {
SectionTitle( SectionTitle(
stringResource(R.string.interface_), padding = screenPadding) stringResource(R.string.interface_),
padding = screenPadding,
)
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.tunnelName, value = uiState.tunnelName,
onValueChange = { value -> viewModel.onTunnelNameChange(value) }, onValueChange = { value -> viewModel.onTunnelNameChange(value) },
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.name), label = stringResource(R.string.name),
hint = stringResource(R.string.tunnel_name).lowercase(), hint = stringResource(R.string.tunnel_name).lowercase(),
modifier = modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
Modifier )
.fillMaxWidth()
.focusRequester(focusRequester))
OutlinedTextField( OutlinedTextField(
modifier = modifier = Modifier.fillMaxWidth().clickable { showAuthPrompt = true },
Modifier.fillMaxWidth().clickable {
showAuthPrompt = true
},
value = uiState.interfaceProxy.privateKey, value = uiState.interfaceProxy.privateKey,
visualTransformation = visualTransformation =
if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated)
isAuthenticated)
VisualTransformation.None VisualTransformation.None
else PasswordVisualTransformation(), else PasswordVisualTransformation(),
enabled = enabled = (id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
(id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
onValueChange = { value -> viewModel.onPrivateKeyChange(value) }, onValueChange = { value -> viewModel.onPrivateKeyChange(value) },
trailingIcon = { trailingIcon = {
IconButton( IconButton(
modifier = Modifier.focusRequester(FocusRequester.Default), modifier = Modifier.focusRequester(FocusRequester.Default),
onClick = { viewModel.generateKeyPair() }) { onClick = { viewModel.generateKeyPair() },
) {
Icon( Icon(
Icons.Rounded.Refresh, Icons.Rounded.Refresh,
stringResource(R.string.rotate_keys), stringResource(R.string.rotate_keys),
tint = Color.White) tint = Color.White,
)
} }
}, },
label = { Text(stringResource(R.string.private_key)) }, label = { Text(stringResource(R.string.private_key)) },
singleLine = true, singleLine = true,
placeholder = { Text(stringResource(R.string.base64_key)) }, placeholder = { Text(stringResource(R.string.base64_key)) },
keyboardOptions = keyboardOptions, keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions) keyboardActions = keyboardActions,
)
OutlinedTextField( OutlinedTextField(
modifier = modifier =
Modifier Modifier.fillMaxWidth().focusRequester(FocusRequester.Default),
.fillMaxWidth()
.focusRequester(FocusRequester.Default),
value = uiState.interfaceProxy.publicKey, value = uiState.interfaceProxy.publicKey,
enabled = false, enabled = false,
onValueChange = {}, onValueChange = {},
@ -370,67 +401,64 @@ fun ConfigScreen(
modifier = Modifier.focusRequester(FocusRequester.Default), modifier = Modifier.focusRequester(FocusRequester.Default),
onClick = { onClick = {
clipboardManager.setText( clipboardManager.setText(
AnnotatedString(uiState.interfaceProxy.publicKey)) AnnotatedString(uiState.interfaceProxy.publicKey),
}) { )
},
) {
Icon( Icon(
Icons.Rounded.ContentCopy, Icons.Rounded.ContentCopy,
stringResource(R.string.copy_public_key), stringResource(R.string.copy_public_key),
tint = Color.White) tint = Color.White,
)
} }
}, },
label = { Text(stringResource(R.string.public_key)) }, label = { Text(stringResource(R.string.public_key)) },
singleLine = true, singleLine = true,
placeholder = { Text(stringResource(R.string.base64_key)) }, placeholder = { Text(stringResource(R.string.base64_key)) },
keyboardOptions = keyboardOptions, keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions) keyboardActions = keyboardActions,
)
Row(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.fillMaxWidth()) {
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.addresses, value = uiState.interfaceProxy.addresses,
onValueChange = { value -> onValueChange = { value -> viewModel.onAddressesChanged(value) },
viewModel.onAddressesChanged(value)
},
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.addresses), label = stringResource(R.string.addresses),
hint = stringResource(R.string.comma_separated_list), hint = stringResource(R.string.comma_separated_list),
modifier = modifier = Modifier.fillMaxWidth(3 / 5f).padding(end = 5.dp),
Modifier )
.fillMaxWidth(3 / 5f)
.padding(end = 5.dp))
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.listenPort, value = uiState.interfaceProxy.listenPort,
onValueChange = { value -> onValueChange = { value -> viewModel.onListenPortChanged(value) },
viewModel.onListenPortChanged(value)
},
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.listen_port), label = stringResource(R.string.listen_port),
hint = stringResource(R.string.random), hint = stringResource(R.string.random),
modifier = Modifier.width(IntrinsicSize.Min)) modifier = Modifier.width(IntrinsicSize.Min),
)
} }
Row(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.fillMaxWidth()) {
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.dnsServers, value = uiState.interfaceProxy.dnsServers,
onValueChange = { value -> onValueChange = { value -> viewModel.onDnsServersChanged(value) },
viewModel.onDnsServersChanged(value)
},
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.dns_servers), label = stringResource(R.string.dns_servers),
hint = stringResource(R.string.comma_separated_list), hint = stringResource(R.string.comma_separated_list),
modifier = modifier = Modifier.fillMaxWidth(3 / 5f).padding(end = 5.dp),
Modifier )
.fillMaxWidth(3 / 5f)
.padding(end = 5.dp))
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.mtu, value = uiState.interfaceProxy.mtu,
onValueChange = { value -> viewModel.onMtuChanged(value) }, onValueChange = { value -> viewModel.onMtuChanged(value) },
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.mtu), label = stringResource(R.string.mtu),
hint = stringResource(R.string.auto), hint = stringResource(R.string.auto),
modifier = Modifier.width(IntrinsicSize.Min)) modifier = Modifier.width(IntrinsicSize.Min),
)
} }
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize().padding(top = 5.dp), modifier = Modifier.fillMaxSize().padding(top = 5.dp),
horizontalArrangement = Arrangement.Center) { horizontalArrangement = Arrangement.Center,
) {
TextButton(onClick = { showApplicationsDialog = true }) { TextButton(onClick = { showApplicationsDialog = true }) {
Text(applicationButtonText()) Text(applicationButtonText())
} }
@ -449,18 +477,22 @@ fun ConfigScreen(
} else { } else {
Modifier.fillMaxWidth(fillMaxWidth) Modifier.fillMaxWidth(fillMaxWidth)
}) })
.padding(top = 10.dp, bottom = 10.dp)) { .padding(top = 10.dp, bottom = 10.dp),
) {
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = modifier = Modifier.padding(horizontal = 15.dp).padding(bottom = 10.dp),
Modifier.padding(horizontal = 15.dp).padding(bottom = 10.dp)) { ) {
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(horizontal = 5.dp)) { modifier = Modifier.fillMaxWidth().padding(horizontal = 5.dp),
) {
SectionTitle( SectionTitle(
stringResource(R.string.peer), padding = screenPadding) stringResource(R.string.peer),
padding = screenPadding,
)
IconButton(onClick = { viewModel.onDeletePeer(index) }) { IconButton(onClick = { viewModel.onDeletePeer(index) }) {
Icon(Icons.Rounded.Delete, stringResource(R.string.delete)) Icon(Icons.Rounded.Delete, stringResource(R.string.delete))
} }
@ -474,7 +506,8 @@ fun ConfigScreen(
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.public_key), label = stringResource(R.string.public_key),
hint = stringResource(R.string.base64_key), hint = stringResource(R.string.base64_key),
modifier = Modifier.fillMaxWidth()) modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox( ConfigurationTextBox(
value = peer.preSharedKey, value = peer.preSharedKey,
onValueChange = { value -> onValueChange = { value ->
@ -483,7 +516,8 @@ fun ConfigScreen(
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.preshared_key), label = stringResource(R.string.preshared_key),
hint = stringResource(R.string.optional), hint = stringResource(R.string.optional),
modifier = Modifier.fillMaxWidth()) modifier = Modifier.fillMaxWidth(),
)
OutlinedTextField( OutlinedTextField(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
value = peer.persistentKeepalive, value = peer.persistentKeepalive,
@ -494,7 +528,8 @@ fun ConfigScreen(
trailingIcon = { trailingIcon = {
Text( Text(
stringResource(R.string.seconds), stringResource(R.string.seconds),
modifier = Modifier.padding(end = 10.dp)) modifier = Modifier.padding(end = 10.dp),
)
}, },
label = { Text(stringResource(R.string.persistent_keepalive)) }, label = { Text(stringResource(R.string.persistent_keepalive)) },
singleLine = true, singleLine = true,
@ -502,7 +537,8 @@ fun ConfigScreen(
Text(stringResource(R.string.optional_no_recommend)) Text(stringResource(R.string.optional_no_recommend))
}, },
keyboardOptions = keyboardOptions, keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions) keyboardActions = keyboardActions,
)
ConfigurationTextBox( ConfigurationTextBox(
value = peer.endpoint, value = peer.endpoint,
onValueChange = { value -> onValueChange = { value ->
@ -511,7 +547,8 @@ fun ConfigScreen(
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.endpoint), label = stringResource(R.string.endpoint),
hint = stringResource(R.string.endpoint).lowercase(), hint = stringResource(R.string.endpoint).lowercase(),
modifier = Modifier.fillMaxWidth()) modifier = Modifier.fillMaxWidth(),
)
OutlinedTextField( OutlinedTextField(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
value = peer.allowedIps, value = peer.allowedIps,
@ -525,17 +562,20 @@ fun ConfigScreen(
Text(stringResource(R.string.comma_separated_list)) Text(stringResource(R.string.comma_separated_list))
}, },
keyboardOptions = keyboardOptions, keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions) keyboardActions = keyboardActions,
)
} }
} }
} }
Row( Row(
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize().padding(bottom = 140.dp)) { modifier = Modifier.fillMaxSize().padding(bottom = 140.dp),
) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center) { horizontalArrangement = Arrangement.Center,
) {
TextButton(onClick = { viewModel.addEmptyPeer() }) { TextButton(onClick = { viewModel.addEmptyPeer() }) {
Text(stringResource(R.string.add_peer)) Text(stringResource(R.string.add_peer))
} }

View File

@ -46,9 +46,11 @@ constructor(
private val _uiState = MutableStateFlow(ConfigUiState()) private val _uiState = MutableStateFlow(ConfigUiState())
val uiState = _uiState.asStateFlow() val uiState = _uiState.asStateFlow()
fun init(tunnelId : String) = viewModelScope.launch(Dispatchers.IO) { fun init(tunnelId: String) =
viewModelScope.launch(Dispatchers.IO) {
val packages = getQueriedPackages("") val packages = getQueriedPackages("")
val state = if(tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) { val state =
if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
val tunnelConfig = val tunnelConfig =
tunnelConfigRepository.getAll().firstOrNull { it.id.toString() == tunnelId } tunnelConfigRepository.getAll().firstOrNull { it.id.toString() == tunnelId }
if (tunnelConfig != null) { if (tunnelConfig != null) {
@ -76,7 +78,8 @@ constructor(
isAllApplicationsEnabled, isAllApplicationsEnabled,
false, false,
tunnelConfig, tunnelConfig,
tunnelConfig.name) tunnelConfig.name,
)
} else { } else {
ConfigUiState(loading = false, packages = packages) ConfigUiState(loading = false, packages = packages)
} }
@ -85,6 +88,7 @@ constructor(
} }
_uiState.value = state _uiState.value = state
} }
fun onTunnelNameChange(name: String) { fun onTunnelNameChange(name: String) {
_uiState.value = _uiState.value.copy(tunnelName = name) _uiState.value = _uiState.value.copy(tunnelName = name)
} }
@ -95,7 +99,9 @@ constructor(
fun onAddCheckedPackage(packageName: String) { fun onAddCheckedPackage(packageName: String) {
_uiState.value = _uiState.value =
_uiState.value.copy(checkedPackageNames = _uiState.value.checkedPackageNames + packageName) _uiState.value.copy(
checkedPackageNames = _uiState.value.checkedPackageNames + packageName
)
} }
fun onAllApplicationsChange(isAllApplicationsEnabled: Boolean) { fun onAllApplicationsChange(isAllApplicationsEnabled: Boolean) {
@ -104,7 +110,9 @@ constructor(
fun onRemoveCheckedPackage(packageName: String) { fun onRemoveCheckedPackage(packageName: String) {
_uiState.value = _uiState.value =
_uiState.value.copy(checkedPackageNames = _uiState.value.checkedPackageNames - packageName) _uiState.value.copy(
checkedPackageNames = _uiState.value.checkedPackageNames - packageName
)
} }
private fun getQueriedPackages(query: String): List<PackageInfo> { private fun getQueriedPackages(query: String): List<PackageInfo> {
@ -124,7 +132,9 @@ constructor(
private fun getPackagesHoldingPermissions(permissions: Array<String>): List<PackageInfo> { private fun getPackagesHoldingPermissions(permissions: Array<String>): List<PackageInfo> {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getPackagesHoldingPermissions( packageManager.getPackagesHoldingPermissions(
permissions, PackageManager.PackageInfoFlags.of(0L)) permissions,
PackageManager.PackageInfoFlags.of(0L),
)
} else { } else {
packageManager.getPackagesHoldingPermissions(permissions, 0) packageManager.getPackagesHoldingPermissions(permissions, 0)
} }
@ -135,9 +145,7 @@ constructor(
} }
private fun saveConfig(tunnelConfig: TunnelConfig) = private fun saveConfig(tunnelConfig: TunnelConfig) =
viewModelScope.launch { viewModelScope.launch { tunnelConfigRepository.save(tunnelConfig) }
tunnelConfigRepository.save(tunnelConfig)
}
private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) = private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) =
viewModelScope.launch { viewModelScope.launch {
@ -179,7 +187,9 @@ constructor(
val builder = Interface.Builder() val builder = Interface.Builder()
builder.parsePrivateKey(_uiState.value.interfaceProxy.privateKey.trim()) builder.parsePrivateKey(_uiState.value.interfaceProxy.privateKey.trim())
builder.parseAddresses(_uiState.value.interfaceProxy.addresses.trim()) builder.parseAddresses(_uiState.value.interfaceProxy.addresses.trim())
if (_uiState.value.interfaceProxy.dnsServers.isNotEmpty()) {
builder.parseDnsServers(_uiState.value.interfaceProxy.dnsServers.trim()) builder.parseDnsServers(_uiState.value.interfaceProxy.dnsServers.trim())
}
if (_uiState.value.interfaceProxy.mtu.isNotEmpty()) if (_uiState.value.interfaceProxy.mtu.isNotEmpty())
builder.parseMtu(_uiState.value.interfaceProxy.mtu.trim()) builder.parseMtu(_uiState.value.interfaceProxy.mtu.trim())
if (_uiState.value.interfaceProxy.listenPort.isNotEmpty()) { if (_uiState.value.interfaceProxy.listenPort.isNotEmpty()) {
@ -198,7 +208,9 @@ constructor(
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build() val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
val tunnelConfig = val tunnelConfig =
_uiState.value.tunnel?.copy( _uiState.value.tunnel?.copy(
name = _uiState.value.tunnelName, wgQuick = config.toWgQuickString()) name = _uiState.value.tunnelName,
wgQuick = config.toWgQuickString(),
)
updateTunnelConfig(tunnelConfig) updateTunnelConfig(tunnelConfig)
Result.Success(Event.Message.ConfigSaved) Result.Success(Event.Message.ConfigSaved)
} catch (e: Exception) { } catch (e: Exception) {
@ -211,7 +223,10 @@ constructor(
_uiState.value.copy( _uiState.value.copy(
proxyPeers = proxyPeers =
_uiState.value.proxyPeers.update( _uiState.value.proxyPeers.update(
index, _uiState.value.proxyPeers[index].copy(publicKey = value))) index,
_uiState.value.proxyPeers[index].copy(publicKey = value),
),
)
} }
fun onPreSharedKeyChange(index: Int, value: String) { fun onPreSharedKeyChange(index: Int, value: String) {
@ -219,7 +234,10 @@ constructor(
_uiState.value.copy( _uiState.value.copy(
proxyPeers = proxyPeers =
_uiState.value.proxyPeers.update( _uiState.value.proxyPeers.update(
index, _uiState.value.proxyPeers[index].copy(preSharedKey = value))) index,
_uiState.value.proxyPeers[index].copy(preSharedKey = value),
),
)
} }
fun onEndpointChange(index: Int, value: String) { fun onEndpointChange(index: Int, value: String) {
@ -227,7 +245,10 @@ constructor(
_uiState.value.copy( _uiState.value.copy(
proxyPeers = proxyPeers =
_uiState.value.proxyPeers.update( _uiState.value.proxyPeers.update(
index, _uiState.value.proxyPeers[index].copy(endpoint = value))) index,
_uiState.value.proxyPeers[index].copy(endpoint = value),
),
)
} }
fun onAllowedIpsChange(index: Int, value: String) { fun onAllowedIpsChange(index: Int, value: String) {
@ -235,7 +256,10 @@ constructor(
_uiState.value.copy( _uiState.value.copy(
proxyPeers = proxyPeers =
_uiState.value.proxyPeers.update( _uiState.value.proxyPeers.update(
index, _uiState.value.proxyPeers[index].copy(allowedIps = value))) index,
_uiState.value.proxyPeers[index].copy(allowedIps = value),
),
)
} }
fun onPersistentKeepaliveChanged(index: Int, value: String) { fun onPersistentKeepaliveChanged(index: Int, value: String) {
@ -243,12 +267,16 @@ constructor(
_uiState.value.copy( _uiState.value.copy(
proxyPeers = proxyPeers =
_uiState.value.proxyPeers.update( _uiState.value.proxyPeers.update(
index, _uiState.value.proxyPeers[index].copy(persistentKeepalive = value))) index,
_uiState.value.proxyPeers[index].copy(persistentKeepalive = value),
),
)
} }
fun onDeletePeer(index: Int) { fun onDeletePeer(index: Int) {
_uiState.value = _uiState.value.copy( _uiState.value =
proxyPeers = _uiState.value.proxyPeers.removeAt(index) _uiState.value.copy(
proxyPeers = _uiState.value.proxyPeers.removeAt(index),
) )
} }
@ -263,22 +291,30 @@ constructor(
interfaceProxy = interfaceProxy =
_uiState.value.interfaceProxy.copy( _uiState.value.interfaceProxy.copy(
privateKey = keyPair.privateKey.toBase64(), privateKey = keyPair.privateKey.toBase64(),
publicKey = keyPair.publicKey.toBase64())) publicKey = keyPair.publicKey.toBase64(),
),
)
} }
fun onAddressesChanged(value: String) { fun onAddressesChanged(value: String) {
_uiState.value = _uiState.value =
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value)) _uiState.value.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value)
)
} }
fun onListenPortChanged(value: String) { fun onListenPortChanged(value: String) {
_uiState.value = _uiState.value =
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value)) _uiState.value.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value)
)
} }
fun onDnsServersChanged(value: String) { fun onDnsServersChanged(value: String) {
_uiState.value = _uiState.value =
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value)) _uiState.value.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value)
)
} }
fun onMtuChanged(value: String) { fun onMtuChanged(value: String) {
@ -288,12 +324,16 @@ constructor(
private fun onInterfacePublicKeyChange(value: String) { private fun onInterfacePublicKeyChange(value: String) {
_uiState.value = _uiState.value =
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value)) _uiState.value.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value)
)
} }
fun onPrivateKeyChange(value: String) { fun onPrivateKeyChange(value: String) {
_uiState.value = _uiState.value =
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value)) _uiState.value.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value)
)
if (NumberUtils.isValidKey(value)) { if (NumberUtils.isValidKey(value)) {
val pair = KeyPair(Key.fromBase64(value)) val pair = KeyPair(Key.fromBase64(value))
onInterfacePublicKeyChange(pair.publicKey.toBase64()) onInterfacePublicKeyChange(pair.publicKey.toBase64())

View File

@ -107,6 +107,7 @@ import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@ -127,6 +128,7 @@ fun MainScreen(
var showBottomSheet by remember { mutableStateOf(false) } var showBottomSheet by remember { mutableStateOf(false) }
var showPrimaryChangeAlertDialog by remember { mutableStateOf(false) } var showPrimaryChangeAlertDialog by remember { mutableStateOf(false) }
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) } var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
@ -155,21 +157,28 @@ fun MainScreen(
context.packageManager.queryIntentActivities( context.packageManager.queryIntentActivities(
intent, intent,
PackageManager.ResolveInfoFlags.of( PackageManager.ResolveInfoFlags.of(
PackageManager.MATCH_DEFAULT_ONLY.toLong())) PackageManager.MATCH_DEFAULT_ONLY.toLong(),
),
)
} else { } else {
context.packageManager.queryIntentActivities( context.packageManager.queryIntentActivities(
intent, PackageManager.MATCH_DEFAULT_ONLY) intent,
PackageManager.MATCH_DEFAULT_ONLY,
)
} }
if (activitiesToResolveIntent.all { if (
activitiesToResolveIntent.all {
val name = it.activityInfo.packageName val name = it.activityInfo.packageName
name.startsWith(Constants.GOOGLE_TV_EXPLORER_STUB) || name.startsWith(Constants.GOOGLE_TV_EXPLORER_STUB) ||
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB) name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
}) { }
) {
showSnackbarMessage(Event.Error.FileExplorerRequired.message) showSnackbarMessage(Event.Error.FileExplorerRequired.message)
} }
return intent return intent
} }
}) { data -> },
) { data ->
if (data == null) return@rememberLauncherForActivityResult if (data == null) return@rememberLauncherForActivityResult
scope.launch { scope.launch {
viewModel.onTunnelFileSelected(data).let { viewModel.onTunnelFileSelected(data).let {
@ -194,7 +203,8 @@ fun MainScreen(
} }
} }
} }
}) },
)
AnimatedVisibility(showPrimaryChangeAlertDialog) { AnimatedVisibility(showPrimaryChangeAlertDialog) {
AlertDialog( AlertDialog(
@ -205,7 +215,8 @@ fun MainScreen(
viewModel.onDefaultTunnelChange(selectedTunnel) viewModel.onDefaultTunnelChange(selectedTunnel)
showPrimaryChangeAlertDialog = false showPrimaryChangeAlertDialog = false
selectedTunnel = null selectedTunnel = null
}) { },
) {
Text(text = stringResource(R.string.okay)) Text(text = stringResource(R.string.okay))
} }
}, },
@ -215,7 +226,32 @@ fun MainScreen(
} }
}, },
title = { Text(text = stringResource(R.string.primary_tunnel_change)) }, title = { Text(text = stringResource(R.string.primary_tunnel_change)) },
text = { Text(text = stringResource(R.string.primary_tunnel_change_question)) }) text = { Text(text = stringResource(R.string.primary_tunnel_change_question)) },
)
}
AnimatedVisibility(showDeleteTunnelAlertDialog) {
AlertDialog(
onDismissRequest = { showDeleteTunnelAlertDialog = false },
confirmButton = {
TextButton(
onClick = {
selectedTunnel?.let { viewModel.onDelete(it) }
showDeleteTunnelAlertDialog = false
selectedTunnel = null
},
) {
Text(text = stringResource(R.string.yes))
}
},
dismissButton = {
TextButton(onClick = { showDeleteTunnelAlertDialog = false }) {
Text(text = stringResource(R.string.cancel))
}
},
title = { Text(text = stringResource(R.string.delete_tunnel)) },
text = { Text(text = stringResource(R.string.delete_tunnel_message)) },
)
} }
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) { fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
@ -228,7 +264,8 @@ fun MainScreen(
detectTapGestures( detectTapGestures(
onTap = { onTap = {
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) selectedTunnel = null if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) selectedTunnel = null
}) },
)
}, },
floatingActionButtonPosition = FabPosition.End, floatingActionButtonPosition = FabPosition.End,
topBar = { topBar = {
@ -238,25 +275,37 @@ fun MainScreen(
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.requiredWidth(LocalConfiguration.current.screenWidthDp.dp).padding(end = 5.dp)) { modifier =
Modifier.requiredWidth(LocalConfiguration.current.screenWidthDp.dp)
.padding(end = 5.dp),
) {
Row { Row {
Icon( Icon(
Icons.Rounded.Bolt, Icons.Rounded.Bolt,
stringResource(id = R.string.auto), stringResource(id = R.string.auto),
modifier = Modifier.size(25.dp), modifier = Modifier.size(25.dp),
tint = if(uiState.settings.isAutoTunnelPaused) Color.Gray else mint) tint =
if (uiState.settings.isAutoTunnelPaused) Color.Gray
else mint,
)
Text( Text(
"Auto-tunneling: ${if (uiState.settings.isAutoTunnelPaused) "paused" else "active"}", "Auto-tunneling: ${if (uiState.settings.isAutoTunnelPaused) "paused" else "active"}",
style = typography.bodyLarge, style = typography.bodyLarge,
modifier = Modifier.padding(start = 10.dp)) modifier = Modifier.padding(start = 10.dp),
)
} }
if(uiState.settings.isAutoTunnelPaused) TextButton( if (uiState.settings.isAutoTunnelPaused)
TextButton(
onClick = { viewModel.resumeAutoTunneling() }, onClick = { viewModel.resumeAutoTunneling() },
modifier = Modifier.padding(end = 10.dp)) { modifier = Modifier.padding(end = 10.dp),
) {
Text("Resume") Text("Resume")
} else TextButton( }
else
TextButton(
onClick = { viewModel.pauseAutoTunneling() }, onClick = { viewModel.pauseAutoTunneling() },
modifier = Modifier.padding(end = 10.dp)) { modifier = Modifier.padding(end = 10.dp),
) {
Text("Pause") Text("Pause")
} }
} }
@ -267,14 +316,17 @@ fun MainScreen(
AnimatedVisibility( AnimatedVisibility(
visible = isVisible.value, visible = isVisible.value,
enter = slideInVertically(initialOffsetY = { it * 2 }), enter = slideInVertically(initialOffsetY = { it * 2 }),
exit = slideOutVertically(targetOffsetY = { it * 2 })) { exit = slideOutVertically(targetOffsetY = { it * 2 }),
) {
val secondaryColor = MaterialTheme.colorScheme.secondary val secondaryColor = MaterialTheme.colorScheme.secondary
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
var fobColor by remember { mutableStateOf(secondaryColor) } var fobColor by remember { mutableStateOf(secondaryColor) }
FloatingActionButton( FloatingActionButton(
modifier = modifier =
(if (WireGuardAutoTunnel.isRunningOnAndroidTv() && (if (
uiState.tunnels.isEmpty()) WireGuardAutoTunnel.isRunningOnAndroidTv() &&
uiState.tunnels.isEmpty()
)
Modifier.focusRequester(focusRequester) Modifier.focusRequester(focusRequester)
else Modifier) else Modifier)
.padding(bottom = 90.dp) .padding(bottom = 90.dp)
@ -285,25 +337,31 @@ fun MainScreen(
}, },
onClick = { showBottomSheet = true }, onClick = { showBottomSheet = true },
containerColor = fobColor, containerColor = fobColor,
shape = RoundedCornerShape(16.dp)) { shape = RoundedCornerShape(16.dp),
) {
Icon( Icon(
imageVector = Icons.Rounded.Add, imageVector = Icons.Rounded.Add,
contentDescription = stringResource(id = R.string.add_tunnel), contentDescription = stringResource(id = R.string.add_tunnel),
tint = Color.DarkGray) tint = Color.DarkGray,
)
} }
} }
}) { innerPadding -> },
) { innerPadding ->
AnimatedVisibility(uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) { AnimatedVisibility(uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize().padding(padding)) { modifier = Modifier.fillMaxSize().padding(padding),
) {
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic) Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
} }
} }
if (showBottomSheet) { if (showBottomSheet) {
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = { showBottomSheet = false }, sheetState = sheetState) { onDismissRequest = { showBottomSheet = false },
sheetState = sheetState,
) {
// Sheet content // Sheet content
Row( Row(
modifier = modifier =
@ -312,14 +370,17 @@ fun MainScreen(
showBottomSheet = false showBottomSheet = false
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES) tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
} }
.padding(10.dp)) { .padding(10.dp),
) {
Icon( Icon(
Icons.Filled.FileOpen, Icons.Filled.FileOpen,
contentDescription = stringResource(id = R.string.open_file), contentDescription = stringResource(id = R.string.open_file),
modifier = Modifier.padding(10.dp)) modifier = Modifier.padding(10.dp),
)
Text( Text(
stringResource(id = R.string.add_tunnels_text), stringResource(id = R.string.add_tunnels_text),
modifier = Modifier.padding(10.dp)) modifier = Modifier.padding(10.dp),
)
} }
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) { if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Divider() Divider()
@ -332,20 +393,26 @@ fun MainScreen(
val scanOptions = ScanOptions() val scanOptions = ScanOptions()
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE) scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
scanOptions.setOrientationLocked(true) scanOptions.setOrientationLocked(true)
scanOptions.setPrompt(context.getString(R.string.scanning_qr)) scanOptions.setPrompt(
context.getString(R.string.scanning_qr)
)
scanOptions.setBeepEnabled(false) scanOptions.setBeepEnabled(false)
scanOptions.captureActivity = CaptureActivityPortrait::class.java scanOptions.captureActivity =
CaptureActivityPortrait::class.java
scanLauncher.launch(scanOptions) scanLauncher.launch(scanOptions)
} }
} }
.padding(10.dp)) { .padding(10.dp),
) {
Icon( Icon(
Icons.Filled.QrCode, Icons.Filled.QrCode,
contentDescription = stringResource(id = R.string.qr_scan), contentDescription = stringResource(id = R.string.qr_scan),
modifier = Modifier.padding(10.dp)) modifier = Modifier.padding(10.dp),
)
Text( Text(
stringResource(id = R.string.add_from_qr), stringResource(id = R.string.add_from_qr),
modifier = Modifier.padding(10.dp)) modifier = Modifier.padding(10.dp),
)
} }
} }
Divider() Divider()
@ -355,16 +422,20 @@ fun MainScreen(
.clickable { .clickable {
showBottomSheet = false showBottomSheet = false
navController.navigate( navController.navigate(
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}") "${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}",
)
} }
.padding(10.dp)) { .padding(10.dp),
) {
Icon( Icon(
Icons.Filled.Create, Icons.Filled.Create,
contentDescription = stringResource(id = R.string.create_import), contentDescription = stringResource(id = R.string.create_import),
modifier = Modifier.padding(10.dp)) modifier = Modifier.padding(10.dp),
)
Text( Text(
stringResource(id = R.string.create_import), stringResource(id = R.string.create_import),
modifier = Modifier.padding(10.dp)) modifier = Modifier.padding(10.dp),
)
} }
} }
} }
@ -373,16 +444,24 @@ fun MainScreen(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = modifier =
Modifier.fillMaxWidth().fillMaxHeight(.90f).overscroll(ScrollableDefaults.overscrollEffect()).padding(innerPadding), Modifier.fillMaxWidth()
.fillMaxHeight(.90f)
.overscroll(ScrollableDefaults.overscrollEffect())
.padding(innerPadding),
state = rememberLazyListState(0, uiState.tunnels.count()), state = rememberLazyListState(0, uiState.tunnels.count()),
userScrollEnabled = true, userScrollEnabled = true,
reverseLayout = true, reverseLayout = true,
flingBehavior = ScrollableDefaults.flingBehavior()) { flingBehavior = ScrollableDefaults.flingBehavior(),
items(uiState.tunnels, ) {
key = { tunnel -> tunnel.id }) { tunnel -> items(
uiState.tunnels,
key = { tunnel -> tunnel.id },
) { tunnel ->
val leadingIconColor = val leadingIconColor =
(if (uiState.vpnState.name == tunnel.name && (if (
uiState.vpnState.status == Tunnel.State.UP) { uiState.vpnState.name == tunnel.name &&
uiState.vpnState.status == Tunnel.State.UP
) {
uiState.vpnState.statistics uiState.vpnState.statistics
?.mapPeerStats() ?.mapPeerStats()
?.map { it.value?.handshakeStatus() } ?.map { it.value?.handshakeStatus() }
@ -408,19 +487,23 @@ fun MainScreen(
Icons.Rounded.Star, Icons.Rounded.Star,
stringResource(R.string.status), stringResource(R.string.status),
tint = leadingIconColor, tint = leadingIconColor,
modifier = Modifier.padding(end = 10.dp).size(20.dp)) modifier = Modifier.padding(end = 10.dp).size(20.dp),
)
} else { } else {
Icon( Icon(
Icons.Rounded.Circle, Icons.Rounded.Circle,
stringResource(R.string.status), stringResource(R.string.status),
tint = leadingIconColor, tint = leadingIconColor,
modifier = Modifier.padding(end = 15.dp).size(15.dp)) modifier = Modifier.padding(end = 15.dp).size(15.dp),
)
} }
}, },
text = tunnel.name, text = tunnel.name,
onHold = { onHold = {
if ((uiState.vpnState.status == Tunnel.State.UP) && if (
(tunnel.name == uiState.vpnState.name)) { (uiState.vpnState.status == Tunnel.State.UP) &&
(tunnel.name == uiState.vpnState.name)
) {
showSnackbarMessage(Event.Message.TunnelOffAction.message) showSnackbarMessage(Event.Message.TunnelOffAction.message)
return@RowListItem return@RowListItem
} }
@ -429,8 +512,10 @@ fun MainScreen(
}, },
onClick = { onClick = {
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) { if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
if (uiState.vpnState.status == Tunnel.State.UP && if (
(uiState.vpnState.name == tunnel.name)) { uiState.vpnState.status == Tunnel.State.UP &&
(uiState.vpnState.name == tunnel.name)
) {
expanded.value = !expanded.value expanded.value = !expanded.value
} }
} else { } else {
@ -441,38 +526,56 @@ fun MainScreen(
statistics = uiState.vpnState.statistics, statistics = uiState.vpnState.statistics,
expanded = expanded.value, expanded = expanded.value,
rowButton = { rowButton = {
if (tunnel.id == selectedTunnel?.id && if (
!WireGuardAutoTunnel.isRunningOnAndroidTv()) { tunnel.id == selectedTunnel?.id &&
!WireGuardAutoTunnel.isRunningOnAndroidTv()
) {
Row { Row {
if (!uiState.settings.isTunnelConfigDefault(tunnel)) { if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
IconButton( IconButton(
onClick = { onClick = {
if (uiState.settings.isAutoTunnelEnabled && !uiState.settings.isAutoTunnelPaused) { if (
uiState.settings.isAutoTunnelEnabled &&
!uiState.settings.isAutoTunnelPaused
) {
showSnackbarMessage( showSnackbarMessage(
Event.Message.AutoTunnelOffAction.message) Event.Message.AutoTunnelOffAction.message,
)
} else { } else {
showPrimaryChangeAlertDialog = true showPrimaryChangeAlertDialog = true
} }
}) { },
) {
Icon( Icon(
Icons.Rounded.Star, Icons.Rounded.Star,
stringResource(id = R.string.set_primary)) stringResource(id = R.string.set_primary),
)
} }
} }
IconButton( IconButton(
onClick = { onClick = {
if (uiState.settings.isAutoTunnelEnabled && uiState.settings.isTunnelConfigDefault(tunnel) if (
&& !uiState.settings.isAutoTunnelPaused) { uiState.settings.isAutoTunnelEnabled &&
uiState.settings.isTunnelConfigDefault(
tunnel,
) &&
!uiState.settings.isAutoTunnelPaused
) {
showSnackbarMessage( showSnackbarMessage(
Event.Message.AutoTunnelOffAction.message) Event.Message.AutoTunnelOffAction.message,
} else navController.navigate( )
"${Screen.Config.route}/${selectedTunnel?.id}") } else
}) { navController.navigate(
"${Screen.Config.route}/${selectedTunnel?.id}",
)
},
) {
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit)) Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
} }
IconButton( IconButton(
modifier = Modifier.focusable(), modifier = Modifier.focusable(),
onClick = { viewModel.onDelete(tunnel) }) { onClick = { showDeleteTunnelAlertDialog = true },
) {
Icon(Icons.Rounded.Delete, stringResource(id = R.string.delete)) Icon(Icons.Rounded.Delete, stringResource(id = R.string.delete))
} }
} }
@ -493,7 +596,8 @@ fun MainScreen(
onCheckedChange = { checked -> onCheckedChange = { checked ->
if (!checked) expanded.value = false if (!checked) expanded.value = false
onTunnelToggle(checked, tunnel) onTunnelToggle(checked, tunnel)
}) },
)
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Row { Row {
if (!uiState.settings.isTunnelConfigDefault(tunnel)) { if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
@ -501,53 +605,73 @@ fun MainScreen(
onClick = { onClick = {
if (uiState.settings.isAutoTunnelEnabled) { if (uiState.settings.isAutoTunnelEnabled) {
showSnackbarMessage( showSnackbarMessage(
Event.Message.AutoTunnelOffAction.message) Event.Message.AutoTunnelOffAction.message,
)
} else { } else {
selectedTunnel = tunnel selectedTunnel = tunnel
showPrimaryChangeAlertDialog = true showPrimaryChangeAlertDialog = true
} }
}) { },
) {
Icon( Icon(
Icons.Rounded.Star, Icons.Rounded.Star,
stringResource(id = R.string.set_primary)) stringResource(id = R.string.set_primary),
)
} }
} }
IconButton( IconButton(
modifier = Modifier.focusRequester(focusRequester), modifier = Modifier.focusRequester(focusRequester),
onClick = { onClick = {
if (uiState.vpnState.status == Tunnel.State.UP && if (
(uiState.vpnState.name == tunnel.name)) { uiState.vpnState.status == Tunnel.State.UP &&
(uiState.vpnState.name == tunnel.name)
) {
expanded.value = !expanded.value expanded.value = !expanded.value
} else { } else {
showSnackbarMessage(Event.Message.TunnelOnAction.message) showSnackbarMessage(
Event.Message.TunnelOnAction.message
)
} }
}) { },
) {
Icon(Icons.Rounded.Info, stringResource(R.string.info)) Icon(Icons.Rounded.Info, stringResource(R.string.info))
} }
IconButton( IconButton(
onClick = { onClick = {
if (uiState.vpnState.status == Tunnel.State.UP && if (
tunnel.name == uiState.vpnState.name) { uiState.vpnState.status == Tunnel.State.UP &&
showSnackbarMessage(Event.Message.TunnelOffAction.message) tunnel.name == uiState.vpnState.name
) {
showSnackbarMessage(
Event.Message.TunnelOffAction.message
)
} else { } else {
navController.navigate( navController.navigate(
"${Screen.Config.route}/${tunnel.id}") "${Screen.Config.route}/${tunnel.id}",
)
} }
}) { },
) {
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit)) Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
} }
IconButton( IconButton(
onClick = { onClick = {
if (uiState.vpnState.status == Tunnel.State.UP && if (
tunnel.name == uiState.vpnState.name) { uiState.vpnState.status == Tunnel.State.UP &&
showSnackbarMessage(Event.Message.TunnelOffAction.message) tunnel.name == uiState.vpnState.name
) {
showSnackbarMessage(
Event.Message.TunnelOffAction.message
)
} else { } else {
viewModel.onDelete(tunnel) showDeleteTunnelAlertDialog = true
} }
}) { },
) {
Icon( Icon(
Icons.Rounded.Delete, Icons.Rounded.Delete,
stringResource(id = R.string.delete)) stringResource(id = R.string.delete),
)
} }
TunnelSwitch() TunnelSwitch()
} }
@ -555,7 +679,8 @@ fun MainScreen(
TunnelSwitch() TunnelSwitch()
} }
} }
}) },
)
} }
} }
} }

View File

@ -14,11 +14,7 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceState
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.NumberUtils
@ -32,6 +28,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.InputStream import java.io.InputStream
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
import javax.inject.Inject import javax.inject.Inject
@ -58,21 +55,21 @@ constructor(
.stateIn( .stateIn(
viewModelScope, viewModelScope,
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
MainUiState()) MainUiState(),
)
private fun validateWatcherServiceState(settings: Settings) = viewModelScope.launch(Dispatchers.IO) { private fun validateWatcherServiceState(settings: Settings) =
val watcherState = viewModelScope.launch(Dispatchers.IO) {
ServiceManager.getServiceState( if (settings.isAutoTunnelEnabled) {
application.applicationContext, WireGuardConnectivityWatcherService::class.java)
if (settings.isAutoTunnelEnabled &&
watcherState == ServiceState.STOPPED) {
ServiceManager.startWatcherService(application.applicationContext) ServiceManager.startWatcherService(application.applicationContext)
} }
} }
private fun stopWatcherService() = viewModelScope.launch(Dispatchers.IO) { private fun stopWatcherService() =
viewModelScope.launch(Dispatchers.IO) {
ServiceManager.stopWatcherService(application.applicationContext) ServiceManager.stopWatcherService(application.applicationContext)
} }
fun onDelete(tunnel: TunnelConfig) { fun onDelete(tunnel: TunnelConfig) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
if (tunnelConfigRepository.count() == 1) { if (tunnelConfigRepository.count() == 1) {
@ -88,26 +85,28 @@ constructor(
} }
} }
fun onTunnelStart(tunnelConfig: TunnelConfig) = viewModelScope.launch(Dispatchers.IO) { fun onTunnelStart(tunnelConfig: TunnelConfig) =
viewModelScope.launch(Dispatchers.IO) {
Timber.d("On start called!")
stopActiveTunnel().await() stopActiveTunnel().await()
startTunnel(tunnelConfig) startTunnel(tunnelConfig)
} }
private fun startTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch(Dispatchers.IO) { private fun startTunnel(tunnelConfig: TunnelConfig) =
viewModelScope.launch(Dispatchers.IO) {
Timber.d("Start tunnel via manager")
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString()) ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
} }
private fun stopActiveTunnel() = private fun stopActiveTunnel() =
viewModelScope.async(Dispatchers.IO) { viewModelScope.async(Dispatchers.IO) {
if (ServiceManager.getServiceState(
application.applicationContext, WireGuardTunnelService::class.java) ==
ServiceState.STARTED) {
onTunnelStop() onTunnelStop()
delay(Constants.TOGGLE_TUNNEL_DELAY) delay(Constants.TOGGLE_TUNNEL_DELAY)
} }
}
fun onTunnelStop() = viewModelScope.launch(Dispatchers.IO) { fun onTunnelStop() =
viewModelScope.launch(Dispatchers.IO) {
Timber.d("Stopping active tunnel")
ServiceManager.stopVpnService(application.applicationContext) ServiceManager.stopVpnService(application.applicationContext)
} }
@ -144,7 +143,8 @@ constructor(
if (isValidUriContentScheme(uri)) { if (isValidUriContentScheme(uri)) {
val fileName = getFileName(application.applicationContext, uri) val fileName = getFileName(application.applicationContext, uri)
when (getFileExtensionFromFileName(fileName)) { when (getFileExtensionFromFileName(fileName)) {
Constants.CONF_FILE_EXTENSION -> saveTunnelFromConfUri(fileName, uri).let { Constants.CONF_FILE_EXTENSION ->
saveTunnelFromConfUri(fileName, uri).let {
when (it) { when (it) {
is Result.Error -> return Result.Error(Event.Error.FileReadFailed) is Result.Error -> return Result.Error(Event.Error.FileReadFailed)
is Result.Success -> return it is Result.Success -> return it
@ -166,7 +166,8 @@ constructor(
ZipInputStream(getInputStreamFromUri(uri)).use { zip -> ZipInputStream(getInputStreamFromUri(uri)).use { zip ->
generateSequence { zip.nextEntry } generateSequence { zip.nextEntry }
.filterNot { .filterNot {
it.isDirectory || getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION it.isDirectory ||
getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
} }
.forEach { .forEach {
val name = getNameFromFileName(it.name) val name = getNameFromFileName(it.name)
@ -193,11 +194,13 @@ constructor(
WireGuardAutoTunnel.requestTileServiceStateUpdate() WireGuardAutoTunnel.requestTileServiceStateUpdate()
} }
fun pauseAutoTunneling() = viewModelScope.launch { fun pauseAutoTunneling() =
viewModelScope.launch {
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = true)) settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
} }
fun resumeAutoTunneling() = viewModelScope.launch { fun resumeAutoTunneling() =
viewModelScope.launch {
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = false)) settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
} }
@ -233,6 +236,7 @@ constructor(
private fun isValidUriContentScheme(uri: Uri): Boolean { private fun isValidUriContentScheme(uri: Uri): Boolean {
return uri.scheme == Constants.URI_CONTENT_SCHEME return uri.scheme == Constants.URI_CONTENT_SCHEME
} }
private fun getFileName(context: Context, uri: Uri): String { private fun getFileName(context: Context, uri: Uri): String {
return getFileNameByCursor(context, uri) ?: NumberUtils.generateRandomTunnelName() return getFileNameByCursor(context, uri) ?: NumberUtils.generateRandomTunnelName()
} }
@ -252,9 +256,11 @@ constructor(
private fun saveSettings(settings: Settings) = private fun saveSettings(settings: Settings) =
viewModelScope.launch(Dispatchers.IO) { settingsRepository.save(settings) } viewModelScope.launch(Dispatchers.IO) { settingsRepository.save(settings) }
fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) = viewModelScope.launch { fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) =
viewModelScope.launch {
if (selectedTunnel != null) { if (selectedTunnel != null) {
saveSettings(uiState.value.settings.copy(defaultTunnel = selectedTunnel.toString())).join() saveSettings(uiState.value.settings.copy(defaultTunnel = selectedTunnel.toString()))
.join()
WireGuardAutoTunnel.requestTileServiceStateUpdate() WireGuardAutoTunnel.requestTileServiceStateUpdate()
} }
} }

View File

@ -1,10 +1,17 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings package com.zaneschepke.wireguardautotunnel.ui.screens.settings
import android.Manifest import android.Manifest
import android.app.Activity
import android.content.Context.POWER_SERVICE
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.PowerManager
import android.provider.Settings
import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
@ -83,7 +90,8 @@ import java.io.File
@OptIn( @OptIn(
ExperimentalPermissionsApi::class, ExperimentalPermissionsApi::class,
ExperimentalLayoutApi::class) ExperimentalLayoutApi::class,
)
@Composable @Composable
fun SettingsScreen( fun SettingsScreen(
viewModel: SettingsViewModel = hiltViewModel(), viewModel: SettingsViewModel = hiltViewModel(),
@ -115,6 +123,16 @@ fun SettingsScreen(
return return
} }
val startForResult =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
val intent = result.data
// Handle the Intent
}
viewModel.setBatteryOptimizeDisableShown()
}
fun exportAllConfigs() { fun exportAllConfigs() {
try { try {
val files = uiState.tunnels.map { File(context.cacheDir, "${it.name}.conf") } val files = uiState.tunnels.map { File(context.cacheDir, "${it.name}.conf") }
@ -129,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() { fun saveTrustedSSID() {
if (currentText.isNotEmpty()) { if (currentText.isNotEmpty()) {
viewModel.onSaveTrustedSSID(currentText).let { viewModel.onSaveTrustedSSID(currentText).let {
@ -159,7 +199,10 @@ fun SettingsScreen(
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if(WireGuardAutoTunnel.isRunningOnAndroidTv() && Build.VERSION.SDK_INT == Build.VERSION_CODES.Q){ if (
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q
) {
checkFineLocationGranted() checkFineLocationGranted()
} else { } else {
val backgroundLocationState = val backgroundLocationState =
@ -185,8 +228,9 @@ fun SettingsScreen(
TextButton( TextButton(
onClick = { onClick = {
showLocationServicesAlertDialog = false showLocationServicesAlertDialog = false
viewModel.toggleAutoTunnel() handleAutoTunnelToggle()
}) { },
) {
Text(text = stringResource(R.string.okay)) Text(text = stringResource(R.string.okay))
} }
}, },
@ -196,28 +240,33 @@ fun SettingsScreen(
} }
}, },
title = { Text(text = stringResource(R.string.location_services_not_detected)) }, title = { Text(text = stringResource(R.string.location_services_not_detected)) },
text = { Text(text = stringResource(R.string.location_services_missing_message)) }) text = { Text(text = stringResource(R.string.location_services_missing_message)) },
)
} }
if (!uiState.isLocationDisclosureShown) { if (!uiState.isLocationDisclosureShown) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = Modifier.fillMaxSize().verticalScroll(scrollState).padding(padding)) { modifier = Modifier.fillMaxSize().verticalScroll(scrollState).padding(padding),
) {
Icon( Icon(
Icons.Rounded.LocationOff, Icons.Rounded.LocationOff,
contentDescription = stringResource(id = R.string.map), contentDescription = stringResource(id = R.string.map),
modifier = Modifier.padding(30.dp).size(128.dp)) modifier = Modifier.padding(30.dp).size(128.dp),
)
Text( Text(
stringResource(R.string.prominent_background_location_title), stringResource(R.string.prominent_background_location_title),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.padding(30.dp), modifier = Modifier.padding(30.dp),
fontSize = 20.sp) fontSize = 20.sp,
)
Text( Text(
stringResource(R.string.prominent_background_location_message), stringResource(R.string.prominent_background_location_message),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.padding(30.dp), modifier = Modifier.padding(30.dp),
fontSize = 15.sp) fontSize = 15.sp,
)
Row( Row(
modifier = modifier =
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
@ -226,7 +275,8 @@ fun SettingsScreen(
Modifier.fillMaxWidth().padding(30.dp) Modifier.fillMaxWidth().padding(30.dp)
}, },
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly) { horizontalArrangement = Arrangement.SpaceEvenly,
) {
TextButton(onClick = { viewModel.setLocationDisclosureShown() }) { TextButton(onClick = { viewModel.setLocationDisclosureShown() }) {
Text(stringResource(id = R.string.no_thanks)) Text(stringResource(id = R.string.no_thanks))
} }
@ -235,7 +285,8 @@ fun SettingsScreen(
onClick = { onClick = {
openSettings() openSettings()
viewModel.setLocationDisclosureShown() viewModel.setLocationDisclosureShown()
}) { },
) {
Text(stringResource(id = R.string.turn_on)) Text(stringResource(id = R.string.turn_on))
} }
} }
@ -255,19 +306,22 @@ fun SettingsScreen(
onFailure = { onFailure = {
showAuthPrompt = false showAuthPrompt = false
showSnackbarMessage(Event.Error.AuthorizationFailed.message) showSnackbarMessage(Event.Error.AuthorizationFailed.message)
}) },
)
} }
if (uiState.tunnels.isEmpty() && uiState.isLocationDisclosureShown) { if (uiState.tunnels.isEmpty() && uiState.isLocationDisclosureShown) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize().padding(padding)) { modifier = Modifier.fillMaxSize().padding(padding),
) {
Text( Text(
stringResource(R.string.one_tunnel_required), stringResource(R.string.one_tunnel_required),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.padding(15.dp), modifier = Modifier.padding(15.dp),
fontStyle = FontStyle.Italic) fontStyle = FontStyle.Italic,
)
} }
} }
if (uiState.isLocationDisclosureShown && uiState.tunnels.isNotEmpty()) { if (uiState.isLocationDisclosureShown && uiState.tunnels.isNotEmpty()) {
@ -276,9 +330,12 @@ fun SettingsScreen(
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = modifier =
Modifier.fillMaxSize().verticalScroll(scrollState).clickable( Modifier.fillMaxSize().verticalScroll(scrollState).clickable(
indication = null, interactionSource = interactionSource) { indication = null,
interactionSource = interactionSource,
) {
focusManager.clearFocus() focusManager.clearFocus()
}) { },
) {
Surface( Surface(
tonalElevation = 2.dp, tonalElevation = 2.dp,
shadowElevation = 2.dp, shadowElevation = 2.dp,
@ -292,14 +349,17 @@ fun SettingsScreen(
} else { } else {
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 60.dp) Modifier.fillMaxWidth(fillMaxWidth).padding(top = 60.dp)
}) })
.padding(bottom = 10.dp)) { .padding(bottom = 10.dp),
) {
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp)) { modifier = Modifier.padding(15.dp),
) {
SectionTitle( SectionTitle(
title = stringResource(id = R.string.auto_tunneling), title = stringResource(id = R.string.auto_tunneling),
padding = screenPadding) padding = screenPadding,
)
ConfigurationToggle( ConfigurationToggle(
stringResource(id = R.string.tunnel_on_wifi), stringResource(id = R.string.tunnel_on_wifi),
enabled = enabled =
@ -308,30 +368,41 @@ fun SettingsScreen(
checked = uiState.settings.isTunnelOnWifiEnabled, checked = uiState.settings.isTunnelOnWifiEnabled,
padding = screenPadding, padding = screenPadding,
onCheckChanged = { viewModel.onToggleTunnelOnWifi() }, onCheckChanged = { viewModel.onToggleTunnelOnWifi() },
modifier = if(uiState.settings.isAutoTunnelEnabled) Modifier else Modifier.focusRequester(focusRequester).focusProperties { down = focusRequester2 }) modifier =
if (uiState.settings.isAutoTunnelEnabled) Modifier
else
Modifier.focusRequester(focusRequester).focusProperties {
down = focusRequester2
},
)
AnimatedVisibility(visible = uiState.settings.isTunnelOnWifiEnabled) { AnimatedVisibility(visible = uiState.settings.isTunnelOnWifiEnabled) {
Column { Column {
FlowRow( FlowRow(
modifier = Modifier.padding(screenPadding).fillMaxWidth(), modifier = Modifier.padding(screenPadding).fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp)) { horizontalArrangement = Arrangement.spacedBy(5.dp),
) {
uiState.settings.trustedNetworkSSIDs.forEach { ssid -> uiState.settings.trustedNetworkSSIDs.forEach { ssid ->
ClickableIconButton( ClickableIconButton(
onClick = { if(WireGuardAutoTunnel.isRunningOnAndroidTv()) { onClick = {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
viewModel.onDeleteTrustedSSID(ssid) viewModel.onDeleteTrustedSSID(ssid)
focusRequester2.requestFocus() focusRequester2.requestFocus()
}}, }
},
onIconClick = { viewModel.onDeleteTrustedSSID(ssid) }, onIconClick = { viewModel.onDeleteTrustedSSID(ssid) },
text = ssid, text = ssid,
icon = Icons.Filled.Close, icon = Icons.Filled.Close,
enabled = enabled =
!(uiState.settings.isAutoTunnelEnabled || !(uiState.settings.isAutoTunnelEnabled ||
uiState.settings.isAlwaysOnVpnEnabled)) uiState.settings.isAlwaysOnVpnEnabled),
)
} }
if (uiState.settings.trustedNetworkSSIDs.isEmpty()) { if (uiState.settings.trustedNetworkSSIDs.isEmpty()) {
Text( Text(
stringResource(R.string.none), stringResource(R.string.none),
fontStyle = FontStyle.Italic, fontStyle = FontStyle.Italic,
color = Color.Gray) color = Color.Gray,
)
} }
} }
OutlinedTextField( OutlinedTextField(
@ -343,14 +414,17 @@ fun SettingsScreen(
label = { Text(stringResource(R.string.add_trusted_ssid)) }, label = { Text(stringResource(R.string.add_trusted_ssid)) },
modifier = modifier =
Modifier.padding( Modifier.padding(
start = screenPadding, top = 5.dp, bottom = 10.dp) start = screenPadding,
.focusRequester(focusRequester2) top = 5.dp,
, bottom = 10.dp,
)
.focusRequester(focusRequester2),
maxLines = 1, maxLines = 1,
keyboardOptions = keyboardOptions =
KeyboardOptions( KeyboardOptions(
capitalization = KeyboardCapitalization.None, capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done), imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }), keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
trailingIcon = { trailingIcon = {
if (currentText != "") { if (currentText != "") {
@ -360,15 +434,23 @@ fun SettingsScreen(
contentDescription = contentDescription =
if (currentText == "") { if (currentText == "") {
stringResource( stringResource(
id = R.string.trusted_ssid_empty_description) id =
R.string
.trusted_ssid_empty_description,
)
} else { } else {
stringResource( stringResource(
id = R.string.trusted_ssid_value_description) id =
R.string
.trusted_ssid_value_description,
)
}, },
tint = MaterialTheme.colorScheme.primary) tint = MaterialTheme.colorScheme.primary,
)
} }
} }
}) },
)
} }
} }
ConfigurationToggle( ConfigurationToggle(
@ -378,7 +460,8 @@ fun SettingsScreen(
uiState.settings.isAlwaysOnVpnEnabled), uiState.settings.isAlwaysOnVpnEnabled),
checked = uiState.settings.isTunnelOnMobileDataEnabled, checked = uiState.settings.isTunnelOnMobileDataEnabled,
padding = screenPadding, padding = screenPadding,
onCheckChanged = { viewModel.onToggleTunnelOnMobileData() }) onCheckChanged = { viewModel.onToggleTunnelOnMobileData() },
)
ConfigurationToggle( ConfigurationToggle(
stringResource(id = R.string.tunnel_on_ethernet), stringResource(id = R.string.tunnel_on_ethernet),
enabled = enabled =
@ -386,7 +469,8 @@ fun SettingsScreen(
uiState.settings.isAlwaysOnVpnEnabled), uiState.settings.isAlwaysOnVpnEnabled),
checked = uiState.settings.isTunnelOnEthernetEnabled, checked = uiState.settings.isTunnelOnEthernetEnabled,
padding = screenPadding, padding = screenPadding,
onCheckChanged = { viewModel.onToggleTunnelOnEthernet() }) onCheckChanged = { viewModel.onToggleTunnelOnEthernet() },
)
ConfigurationToggle( ConfigurationToggle(
stringResource(R.string.battery_saver), stringResource(R.string.battery_saver),
enabled = enabled =
@ -394,31 +478,47 @@ fun SettingsScreen(
uiState.settings.isAlwaysOnVpnEnabled), uiState.settings.isAlwaysOnVpnEnabled),
checked = uiState.settings.isBatterySaverEnabled, checked = uiState.settings.isBatterySaverEnabled,
padding = screenPadding, padding = screenPadding,
onCheckChanged = { viewModel.onToggleBatterySaver() }) onCheckChanged = { viewModel.onToggleBatterySaver() },
)
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = (if(!uiState.settings.isAutoTunnelEnabled) Modifier else Modifier.focusRequester(focusRequester)) modifier =
.fillMaxSize().padding(top = 5.dp), (if (!uiState.settings.isAutoTunnelEnabled) Modifier
horizontalArrangement = Arrangement.Center) { else
Modifier.focusRequester(
focusRequester,
))
.fillMaxSize()
.padding(top = 5.dp),
horizontalArrangement = Arrangement.Center,
) {
TextButton( TextButton(
enabled = !uiState.settings.isAlwaysOnVpnEnabled, enabled = !uiState.settings.isAlwaysOnVpnEnabled,
onClick = { onClick = {
if (uiState.settings.isTunnelOnWifiEnabled && !uiState.settings.isAutoTunnelEnabled) { if (
uiState.settings.isTunnelOnWifiEnabled &&
!uiState.settings.isAutoTunnelEnabled
) {
when (false) { when (false) {
isBackgroundLocationGranted -> isBackgroundLocationGranted ->
showSnackbarMessage(Event.Error.BackgroundLocationRequired.message) showSnackbarMessage(
Event.Error.BackgroundLocationRequired.message
)
fineLocationState.status.isGranted -> fineLocationState.status.isGranted ->
showSnackbarMessage(Event.Error.PreciseLocationRequired.message) showSnackbarMessage(
Event.Error.PreciseLocationRequired.message
)
viewModel.isLocationEnabled(context) -> viewModel.isLocationEnabled(context) ->
showLocationServicesAlertDialog = true showLocationServicesAlertDialog = true
else -> { else -> {
viewModel.toggleAutoTunnel() handleAutoTunnelToggle()
} }
} }
} else { } else {
viewModel.toggleAutoTunnel() handleAutoTunnelToggle()
} }
}) { },
) {
val autoTunnelButtonText = val autoTunnelButtonText =
if (uiState.settings.isAutoTunnelEnabled) { if (uiState.settings.isAutoTunnelEnabled) {
stringResource(R.string.disable_auto_tunnel) stringResource(R.string.disable_auto_tunnel)
@ -436,13 +536,17 @@ fun SettingsScreen(
shadowElevation = 2.dp, shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface, color = MaterialTheme.colorScheme.surface,
modifier = Modifier.fillMaxWidth(fillMaxWidth).padding(vertical = 10.dp)) { modifier = Modifier.fillMaxWidth(fillMaxWidth).padding(vertical = 10.dp),
) {
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp)) { modifier = Modifier.padding(15.dp),
) {
SectionTitle( SectionTitle(
title = stringResource(id = R.string.kernel), padding = screenPadding) title = stringResource(id = R.string.kernel),
padding = screenPadding,
)
ConfigurationToggle( ConfigurationToggle(
stringResource(R.string.use_kernel), stringResource(R.string.use_kernel),
enabled = enabled =
@ -451,12 +555,15 @@ fun SettingsScreen(
(uiState.vpnState.status == Tunnel.State.UP)), (uiState.vpnState.status == Tunnel.State.UP)),
checked = uiState.settings.isKernelEnabled, checked = uiState.settings.isKernelEnabled,
padding = screenPadding, padding = screenPadding,
onCheckChanged = { viewModel.onToggleKernelMode().let { onCheckChanged = {
viewModel.onToggleKernelMode().let {
when (it) { when (it) {
is Result.Error -> showSnackbarMessage(it.error.message) is Result.Error -> showSnackbarMessage(it.error.message)
is Result.Success -> {} is Result.Success -> {}
} }
} }) }
},
)
} }
} }
} }
@ -469,31 +576,40 @@ fun SettingsScreen(
modifier = modifier =
Modifier.fillMaxWidth(fillMaxWidth) Modifier.fillMaxWidth(fillMaxWidth)
.padding(vertical = 10.dp) .padding(vertical = 10.dp)
.padding(bottom = 140.dp)) { .padding(bottom = 140.dp),
) {
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp)) { modifier = Modifier.padding(15.dp),
) {
SectionTitle( SectionTitle(
title = stringResource(id = R.string.other), padding = screenPadding) title = stringResource(id = R.string.other),
padding = screenPadding,
)
ConfigurationToggle( ConfigurationToggle(
stringResource(R.string.always_on_vpn_support), stringResource(R.string.always_on_vpn_support),
enabled = !uiState.settings.isAutoTunnelEnabled, enabled = !uiState.settings.isAutoTunnelEnabled,
checked = uiState.settings.isAlwaysOnVpnEnabled, checked = uiState.settings.isAlwaysOnVpnEnabled,
padding = screenPadding, padding = screenPadding,
onCheckChanged = { viewModel.onToggleAlwaysOnVPN() }) onCheckChanged = { viewModel.onToggleAlwaysOnVPN() },
)
ConfigurationToggle( ConfigurationToggle(
stringResource(R.string.enabled_app_shortcuts), stringResource(R.string.enabled_app_shortcuts),
enabled = true, enabled = true,
checked = uiState.settings.isShortcutsEnabled, checked = uiState.settings.isShortcutsEnabled,
padding = screenPadding, padding = screenPadding,
onCheckChanged = { viewModel.onToggleShortcutsEnabled() }) onCheckChanged = { viewModel.onToggleShortcutsEnabled() },
)
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize().padding(top = 5.dp), modifier = Modifier.fillMaxSize().padding(top = 5.dp),
horizontalArrangement = Arrangement.Center) { horizontalArrangement = Arrangement.Center,
) {
TextButton( TextButton(
enabled = !didExportFiles, onClick = { showAuthPrompt = true }) { enabled = !didExportFiles,
onClick = { showAuthPrompt = true },
) {
Text(stringResource(R.string.export_configs)) Text(stringResource(R.string.export_configs))
} }
} }

View File

@ -9,5 +9,6 @@ data class SettingsUiState(
val tunnels: List<TunnelConfig> = emptyList(), val tunnels: List<TunnelConfig> = emptyList(),
val vpnState: VpnState = VpnState(), val vpnState: VpnState = VpnState(),
val isLocationDisclosureShown: Boolean = true, val isLocationDisclosureShown: Boolean = true,
val isBatteryOptimizeDisableShown: Boolean = false,
val loading: Boolean = true val loading: Boolean = true
) )

View File

@ -36,16 +36,27 @@ constructor(
private val vpnService: VpnService private val vpnService: VpnService
) : ViewModel() { ) : ViewModel() {
val uiState = combine( val uiState =
combine(
settingsRepository.getSettingsFlow(), settingsRepository.getSettingsFlow(),
tunnelConfigRepository.getTunnelConfigsFlow(), tunnelConfigRepository.getTunnelConfigsFlow(),
vpnService.vpnState, vpnService.vpnState,
dataStoreManager.locationDisclosureFlow, dataStoreManager.preferencesFlow,
){ settings, tunnels, tunnelState, locationDisclosure -> ) { settings, tunnels, tunnelState, preferences ->
SettingsUiState(settings, tunnels, tunnelState, locationDisclosure SettingsUiState(
?: false, false) settings,
}.stateIn(viewModelScope, tunnels,
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), SettingsUiState()) tunnelState,
preferences?.get(DataStoreManager.LOCATION_DISCLOSURE_SHOWN) ?: false,
preferences?.get(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN) ?: false,
false
)
}
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
SettingsUiState(),
)
fun onSaveTrustedSSID(ssid: String): Result<Unit> { fun onSaveTrustedSSID(ssid: String): Result<Unit> {
val trimmed = ssid.trim() val trimmed = ssid.trim()
@ -58,29 +69,40 @@ constructor(
} }
} }
fun setLocationDisclosureShown() = viewModelScope.launch { fun setLocationDisclosureShown() =
viewModelScope.launch {
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, true) dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, true)
} }
fun setBatteryOptimizeDisableShown() =
viewModelScope.launch {
dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, true)
}
fun onToggleTunnelOnMobileData() { fun onToggleTunnelOnMobileData() {
saveSettings( saveSettings(
uiState.value.settings.copy( uiState.value.settings.copy(
isTunnelOnMobileDataEnabled = !uiState.value.settings.isTunnelOnMobileDataEnabled isTunnelOnMobileDataEnabled = !uiState.value.settings.isTunnelOnMobileDataEnabled,
) ),
) )
} }
fun onDeleteTrustedSSID(ssid: String) { fun onDeleteTrustedSSID(ssid: String) {
saveSettings(uiState.value.settings.copy( saveSettings(
trustedNetworkSSIDs = (uiState.value.settings.trustedNetworkSSIDs - ssid).toMutableList() uiState.value.settings.copy(
)) trustedNetworkSSIDs =
(uiState.value.settings.trustedNetworkSSIDs - ssid).toMutableList(),
),
)
} }
private suspend fun getDefaultTunnelOrFirst(): String { private suspend fun getDefaultTunnelOrFirst(): String {
return uiState.value.settings.defaultTunnel ?: tunnelConfigRepository.getAll().first().toString() return uiState.value.settings.defaultTunnel
?: tunnelConfigRepository.getAll().first().toString()
} }
fun toggleAutoTunnel() = viewModelScope.launch { fun toggleAutoTunnel() =
viewModelScope.launch {
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused
@ -94,28 +116,30 @@ constructor(
uiState.value.settings.copy( uiState.value.settings.copy(
isAutoTunnelEnabled = !isAutoTunnelEnabled, isAutoTunnelEnabled = !isAutoTunnelEnabled,
isAutoTunnelPaused = isAutoTunnelPaused, isAutoTunnelPaused = isAutoTunnelPaused,
defaultTunnel = getDefaultTunnelOrFirst() defaultTunnel = getDefaultTunnelOrFirst(),
) ),
) )
} }
fun onToggleAlwaysOnVPN() =
fun onToggleAlwaysOnVPN() = viewModelScope.launch { viewModelScope.launch {
val updatedSettings = uiState.value.settings.copy( val updatedSettings =
uiState.value.settings.copy(
isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled, isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled,
defaultTunnel = getDefaultTunnelOrFirst() defaultTunnel = getDefaultTunnelOrFirst(),
) )
saveSettings(updatedSettings) saveSettings(updatedSettings)
} }
private fun saveSettings(settings: Settings) = viewModelScope.launch { private fun saveSettings(settings: Settings) =
settingsRepository.save(settings) viewModelScope.launch { settingsRepository.save(settings) }
}
fun onToggleTunnelOnEthernet() { fun onToggleTunnelOnEthernet() {
saveSettings(uiState.value.settings.copy( saveSettings(
isTunnelOnEthernetEnabled = !uiState.value.settings.isTunnelOnEthernetEnabled uiState.value.settings.copy(
)) isTunnelOnEthernetEnabled = !uiState.value.settings.isTunnelOnEthernetEnabled,
),
)
} }
fun isLocationEnabled(context: Context): Boolean { fun isLocationEnabled(context: Context): Boolean {
@ -126,32 +150,32 @@ constructor(
fun onToggleShortcutsEnabled() { fun onToggleShortcutsEnabled() {
saveSettings( saveSettings(
uiState.value.settings.copy( uiState.value.settings.copy(
isShortcutsEnabled = !uiState.value.settings.isShortcutsEnabled isShortcutsEnabled = !uiState.value.settings.isShortcutsEnabled,
) ),
) )
} }
fun onToggleBatterySaver() { fun onToggleBatterySaver() {
saveSettings( saveSettings(
uiState.value.settings.copy( uiState.value.settings.copy(
isBatterySaverEnabled = !uiState.value.settings.isBatterySaverEnabled isBatterySaverEnabled = !uiState.value.settings.isBatterySaverEnabled,
) ),
) )
} }
private fun saveKernelMode(on: Boolean) { private fun saveKernelMode(on: Boolean) {
saveSettings( saveSettings(
uiState.value.settings.copy( uiState.value.settings.copy(
isKernelEnabled = on isKernelEnabled = on,
) ),
) )
} }
fun onToggleTunnelOnWifi() { fun onToggleTunnelOnWifi() {
saveSettings( saveSettings(
uiState.value.settings.copy( uiState.value.settings.copy(
isTunnelOnWifiEnabled = !uiState.value.settings.isTunnelOnWifiEnabled isTunnelOnWifiEnabled = !uiState.value.settings.isTunnelOnWifiEnabled,
) ),
) )
} }

View File

@ -80,12 +80,16 @@ fun SupportScreen(
fun launchEmail() { fun launchEmail() {
try { try {
val intent = val intent =
Intent(Intent.ACTION_SEND).apply { Intent(Intent.ACTION_SENDTO).apply {
type = Constants.EMAIL_MIME_TYPE type = Constants.EMAIL_MIME_TYPE
putExtra(Intent.EXTRA_EMAIL, context.getString(R.string.my_email)) putExtra(Intent.EXTRA_EMAIL, arrayOf(context.getString(R.string.my_email)))
putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject)) putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
} }
startActivity(context, createChooser(intent, context.getString(R.string.email_chooser)), null) startActivity(
context,
createChooser(intent, context.getString(R.string.email_chooser)),
null,
)
} catch (e: Exception) { } catch (e: Exception) {
showSnackbarMessage(Event.Error.Exception(e).message) showSnackbarMessage(Event.Error.Exception(e).message)
} }
@ -103,7 +107,8 @@ fun SupportScreen(
Modifier.fillMaxSize() Modifier.fillMaxSize()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.focusable() .focusable()
.padding(padding)) { .padding(padding),
) {
Surface( Surface(
tonalElevation = 2.dp, tonalElevation = 2.dp,
shadowElevation = 2.dp, shadowElevation = 2.dp,
@ -117,32 +122,38 @@ fun SupportScreen(
} else { } else {
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 20.dp) Modifier.fillMaxWidth(fillMaxWidth).padding(top = 20.dp)
}) })
.padding(bottom = 25.dp)) { .padding(bottom = 25.dp),
) {
Column(modifier = Modifier.padding(20.dp)) { Column(modifier = Modifier.padding(20.dp)) {
Text( Text(
stringResource(R.string.thank_you), stringResource(R.string.thank_you),
textAlign = TextAlign.Start, textAlign = TextAlign.Start,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 20.dp), modifier = Modifier.padding(bottom = 20.dp),
fontSize = 16.sp) fontSize = 16.sp,
)
Text( Text(
stringResource(id = R.string.support_help_text), stringResource(id = R.string.support_help_text),
textAlign = TextAlign.Start, textAlign = TextAlign.Start,
fontSize = 16.sp, fontSize = 16.sp,
modifier = Modifier.padding(bottom = 20.dp)) modifier = Modifier.padding(bottom = 20.dp),
)
TextButton( TextButton(
onClick = { openWebPage(context.resources.getString(R.string.docs_url)) }, onClick = { openWebPage(context.resources.getString(R.string.docs_url)) },
modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester)) { modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester),
) {
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()) { modifier = Modifier.fillMaxWidth(),
) {
Row { Row {
Icon(Icons.Rounded.Book, stringResource(id = R.string.docs)) Icon(Icons.Rounded.Book, stringResource(id = R.string.docs))
Text( Text(
stringResource(id = R.string.docs_description), stringResource(id = R.string.docs_description),
textAlign = TextAlign.Justify, textAlign = TextAlign.Justify,
modifier = Modifier.padding(start = 10.dp)) modifier = Modifier.padding(start = 10.dp),
)
} }
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go)) Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
} }
@ -150,20 +161,24 @@ fun SupportScreen(
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp) Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
TextButton( TextButton(
onClick = { openWebPage(context.resources.getString(R.string.discord_url)) }, onClick = { openWebPage(context.resources.getString(R.string.discord_url)) },
modifier = Modifier.padding(vertical = 5.dp)) { modifier = Modifier.padding(vertical = 5.dp),
) {
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()) { modifier = Modifier.fillMaxWidth(),
) {
Row { Row {
Icon( Icon(
imageVector = ImageVector.vectorResource(R.drawable.discord), imageVector = ImageVector.vectorResource(R.drawable.discord),
stringResource(id = R.string.discord), stringResource(id = R.string.discord),
Modifier.size(25.dp)) Modifier.size(25.dp),
)
Text( Text(
stringResource(id = R.string.discord_description), stringResource(id = R.string.discord_description),
textAlign = TextAlign.Justify, textAlign = TextAlign.Justify,
modifier = Modifier.padding(start = 10.dp)) modifier = Modifier.padding(start = 10.dp),
)
} }
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go)) Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
} }
@ -171,37 +186,45 @@ fun SupportScreen(
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp) Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
TextButton( TextButton(
onClick = { openWebPage(context.resources.getString(R.string.github_url)) }, onClick = { openWebPage(context.resources.getString(R.string.github_url)) },
modifier = Modifier.padding(vertical = 5.dp)) { modifier = Modifier.padding(vertical = 5.dp),
) {
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()) { modifier = Modifier.fillMaxWidth(),
) {
Row { Row {
Icon( Icon(
imageVector = ImageVector.vectorResource(R.drawable.github), imageVector = ImageVector.vectorResource(R.drawable.github),
stringResource(id = R.string.github), stringResource(id = R.string.github),
Modifier.size(25.dp)) Modifier.size(25.dp),
)
Text( Text(
"Open an issue", "Open an issue",
textAlign = TextAlign.Justify, textAlign = TextAlign.Justify,
modifier = Modifier.padding(start = 10.dp)) modifier = Modifier.padding(start = 10.dp),
)
} }
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go)) Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
} }
} }
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp) Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
TextButton( TextButton(
onClick = { launchEmail() }, modifier = Modifier.padding(vertical = 5.dp)) { onClick = { launchEmail() },
modifier = Modifier.padding(vertical = 5.dp),
) {
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()) { modifier = Modifier.fillMaxWidth(),
) {
Row { Row {
Icon(Icons.Rounded.Mail, stringResource(id = R.string.email)) Icon(Icons.Rounded.Mail, stringResource(id = R.string.email))
Text( Text(
stringResource(id = R.string.email_description), stringResource(id = R.string.email_description),
textAlign = TextAlign.Justify, textAlign = TextAlign.Justify,
modifier = Modifier.padding(start = 10.dp)) modifier = Modifier.padding(start = 10.dp),
)
} }
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go)) Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
} }
@ -216,11 +239,13 @@ fun SupportScreen(
modifier = modifier =
Modifier.clickable { Modifier.clickable {
openWebPage(context.resources.getString(R.string.privacy_policy_url)) openWebPage(context.resources.getString(R.string.privacy_policy_url))
}) },
)
Row( Row(
horizontalArrangement = Arrangement.spacedBy(25.dp), horizontalArrangement = Arrangement.spacedBy(25.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(25.dp)) { modifier = Modifier.padding(25.dp),
) {
Text("Version: ${BuildConfig.VERSION_NAME}", modifier = Modifier.focusable()) Text("Version: ${BuildConfig.VERSION_NAME}", modifier = Modifier.focusable())
Text("Mode: ${if (uiState.settings.isKernelEnabled) "Kernel" else "Userspace"}") Text("Mode: ${if (uiState.settings.isKernelEnabled) "Kernel" else "Userspace"}")
} }

View File

@ -2,7 +2,4 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.support
import com.zaneschepke.wireguardautotunnel.data.model.Settings import com.zaneschepke.wireguardautotunnel.data.model.Settings
data class SupportUiState( data class SupportUiState(val settings: Settings = Settings(), val loading: Boolean = true)
val settings : Settings = Settings(),
val loading : Boolean = true
)

View File

@ -11,15 +11,16 @@ import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class SupportViewModel @Inject constructor( class SupportViewModel @Inject constructor(private val settingsRepository: SettingsRepository) :
private val settingsRepository: SettingsRepository ViewModel() {
) : ViewModel() {
val uiState = settingsRepository.getSettingsFlow().map { val uiState =
SupportUiState(it, false) settingsRepository
}.stateIn( .getSettingsFlow()
.map { SupportUiState(it, false) }
.stateIn(
viewModelScope, viewModelScope,
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
SupportUiState() SupportUiState(),
) )
} }

View File

@ -21,7 +21,7 @@ private val DarkColorScheme =
primary = virdigris, primary = virdigris,
secondary = virdigris, secondary = virdigris,
// secondary = PurpleGrey80, // secondary = PurpleGrey80,
tertiary = virdigris tertiary = virdigris,
// tertiary = Pink80 // tertiary = Pink80
) )
@ -29,7 +29,7 @@ private val LightColorScheme =
lightColorScheme( lightColorScheme(
primary = Purple40, primary = Purple40,
secondary = PurpleGrey40, secondary = PurpleGrey40,
tertiary = Pink40 tertiary = Pink40,
/* Other default colors to override /* Other default colors to override
background = Color(0xFFFFFBFE), background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE), surface = Color(0xFFFFFBFE),
@ -57,7 +57,6 @@ fun WireguardAutoTunnelTheme(
val context = LocalContext.current val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
} }
darkTheme -> DarkColorScheme darkTheme -> DarkColorScheme
else -> LightColorScheme else -> LightColorScheme
} }
@ -68,14 +67,19 @@ fun WireguardAutoTunnelTheme(
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
window.statusBarColor = Color.Transparent.toArgb() window.statusBarColor = Color.Transparent.toArgb()
window.navigationBarColor = Color.Transparent.toArgb() window.navigationBarColor = Color.Transparent.toArgb()
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = !darkTheme WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars =
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightNavigationBars = !darkTheme !darkTheme
WindowCompat.getInsetsController(
window,
window.decorView,
)
.isAppearanceLightNavigationBars = !darkTheme
} }
} }
MaterialTheme( MaterialTheme(
colorScheme = colorScheme, colorScheme = colorScheme,
typography = Typography, typography = Typography,
content = content content = content,
) )
} }

View File

@ -14,7 +14,7 @@ fun TransparentSystemBars() {
DisposableEffect(systemUiController, useDarkIcons) { DisposableEffect(systemUiController, useDarkIcons) {
systemUiController.setSystemBarsColor( systemUiController.setSystemBarsColor(
color = Color.Transparent, color = Color.Transparent,
darkIcons = useDarkIcons darkIcons = useDarkIcons,
) )
onDispose {} onDispose {}

View File

@ -15,8 +15,8 @@ val Typography =
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 16.sp, fontSize = 16.sp,
lineHeight = 24.sp, lineHeight = 24.sp,
letterSpacing = 0.5.sp letterSpacing = 0.5.sp,
) ),
/* Other default text styles to override /* Other default text styles to override
titleLarge = TextStyle( titleLarge = TextStyle(
fontFamily = FontFamily.Default, fontFamily = FontFamily.Default,

View File

@ -12,76 +12,97 @@ sealed class Event {
override val message: String override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_none) get() = WireGuardAutoTunnel.instance.getString(R.string.error_none)
} }
data object SsidConflict : Error() { data object SsidConflict : Error() {
override val message: String override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_ssid_exists) get() = WireGuardAutoTunnel.instance.getString(R.string.error_ssid_exists)
} }
data object RootDenied : Error() { data object RootDenied : Error() {
override val message: String override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_root_denied) get() = WireGuardAutoTunnel.instance.getString(R.string.error_root_denied)
} }
data class General(val customMessage: String) : Error() { data class General(val customMessage: String) : Error() {
override val message: String override val message: String
get() = customMessage get() = customMessage
} }
data class Exception(val exception: kotlin.Exception) : Error() { data class Exception(val exception: kotlin.Exception) : Error() {
override val message: String override val message: String
get() = exception.message ?: WireGuardAutoTunnel.instance.getString(R.string.unknown_error) get() =
exception.message
?: WireGuardAutoTunnel.instance.getString(R.string.unknown_error)
} }
data object InvalidQrCode : Error() { data object InvalidQrCode : Error() {
override val message: String override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_invalid_code) get() = WireGuardAutoTunnel.instance.getString(R.string.error_invalid_code)
} }
data object InvalidFileExtension : Error() { data object InvalidFileExtension : Error() {
override val message: String override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension) get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
} }
data object FileReadFailed : Error() { data object FileReadFailed : Error() {
override val message: String override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension) get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
} }
data object AuthenticationFailed : Error() { data object AuthenticationFailed : Error() {
override val message: String override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authentication_failed) get() = WireGuardAutoTunnel.instance.getString(R.string.error_authentication_failed)
} }
data object AuthorizationFailed : Error() { data object AuthorizationFailed : Error() {
override val message: String override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authorization_failed) get() = WireGuardAutoTunnel.instance.getString(R.string.error_authorization_failed)
} }
data object BackgroundLocationRequired : Error() { data object BackgroundLocationRequired : Error() {
override val message: String override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.background_location_required) get() =
WireGuardAutoTunnel.instance.getString(R.string.background_location_required)
} }
data object LocationServicesRequired : Error() { data object LocationServicesRequired : Error() {
override val message: String override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.location_services_required) get() = WireGuardAutoTunnel.instance.getString(R.string.location_services_required)
} }
data object PreciseLocationRequired : Error() { data object PreciseLocationRequired : Error() {
override val message: String override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.precise_location_required) get() = WireGuardAutoTunnel.instance.getString(R.string.precise_location_required)
} }
data object FileExplorerRequired : Error() { data object FileExplorerRequired : Error() {
override val message: String override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_no_file_explorer) get() = WireGuardAutoTunnel.instance.getString(R.string.error_no_file_explorer)
} }
} }
sealed class Message : Event() { sealed class Message : Event() {
data object ConfigSaved : Message() { data object ConfigSaved : Message() {
override val message: String override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.config_changes_saved) get() = WireGuardAutoTunnel.instance.getString(R.string.config_changes_saved)
} }
data object ConfigsExported : Message() { data object ConfigsExported : Message() {
override val message: String override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.exported_configs_message) get() = WireGuardAutoTunnel.instance.getString(R.string.exported_configs_message)
} }
data object TunnelOffAction : Message() { data object TunnelOffAction : Message() {
override val message: String override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_tunnel) get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_tunnel)
} }
data object TunnelOnAction : Message() { data object TunnelOnAction : Message() {
override val message: String override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_on_tunnel) get() = WireGuardAutoTunnel.instance.getString(R.string.turn_on_tunnel)
} }
data object AutoTunnelOffAction : Message() { data object AutoTunnelOffAction : Message() {
override val message: String override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_auto) get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_auto)

View File

@ -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>.update(index: Int, item: T): List<T> = toMutableList().apply { this[index] = item }
fun <T> List<T>.removeAt(index: Int): List<T> = toMutableList().apply { this.removeAt(index) } fun <T> List<T>.removeAt(index: Int): List<T> = toMutableList().apply { this.removeAt(index) }
typealias TunnelConfigs = List<TunnelConfig> typealias TunnelConfigs = List<TunnelConfig>
typealias Packages = List<PackageInfo> typealias Packages = List<PackageInfo>
fun Statistics.mapPeerStats(): Map<Key, PeerStats?> { fun Statistics.mapPeerStats(): Map<Key, PeerStats?> {
return this.peers().associateWith { key -> return this.peers().associateWith { key -> (this.peer(key)) }
(this.peer(key))
}
} }
fun PeerStats.latestHandshakeSeconds(): Long? { fun PeerStats.latestHandshakeSeconds(): Long? {
@ -65,4 +65,3 @@ fun PeerStats.handshakeStatus() : HandshakeStatus {
} }
} }
} }

View File

@ -36,21 +36,19 @@ object FileUtils {
val target = val target =
File( File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
fileName fileName,
) )
return target.outputStream() return target.outputStream()
} }
return null return null
} }
fun saveFilesToZip( fun saveFilesToZip(context: Context, files: List<File>) {
context: Context, val zipOutputStream =
files: List<File> createDownloadsFileOutputStream(
) {
val zipOutputStream = createDownloadsFileOutputStream(
context, context,
"wg-export_${Instant.now().epochSecond}.zip", "wg-export_${Instant.now().epochSecond}.zip",
ZIP_FILE_MIME_TYPE ZIP_FILE_MIME_TYPE,
) )
ZipOutputStream(zipOutputStream).use { zos -> ZipOutputStream(zipOutputStream).use { zos ->
files.forEach { file -> files.forEach { file ->

View File

@ -2,9 +2,9 @@ package com.zaneschepke.wireguardautotunnel.util
import timber.log.Timber import timber.log.Timber
sealed class Result<T> { sealed class Result<T> {
class Success<T>(val data: T) : Result<T>() class Success<T>(val data: T) : Result<T>()
class Error<T>(val error: Event.Error) : Result<T>() { class Error<T>(val error: Event.Error) : Result<T>() {
init { init {
when (this.error) { when (this.error) {

View File

@ -4,7 +4,7 @@
android:viewportWidth="256" android:viewportWidth="256"
android:viewportHeight="256"> android:viewportHeight="256">
<path <path
android:pathData="M216.86,45.1C200.29,37.34 182.57,31.71 164.04,28.5C161.77,32.61 159.11,38.15 157.28,42.55C137.58,39.58 118.07,39.58 98.74,42.55C96.91,38.15 94.19,32.61 91.9,28.5C73.35,31.71 55.61,37.36 39.04,45.14C5.62,95.65 -3.44,144.9 1.09,193.46C23.26,210.01 44.74,220.07 65.86,226.65C71.08,219.47 75.73,211.84 79.74,203.8C72.1,200.9 64.79,197.32 57.89,193.17C59.72,191.81 61.51,190.39 63.24,188.93C105.37,208.63 151.13,208.63 192.75,188.93C194.51,190.39 196.3,191.81 198.11,193.17C191.18,197.34 183.85,200.92 176.22,203.82C180.23,211.84 184.86,219.49 190.1,226.67C211.24,220.09 232.74,210.03 254.91,193.46C260.23,137.17 245.83,88.37 216.86,45.1ZM85.47,163.59C72.83,163.59 62.46,151.79 62.46,137.41C62.46,123.04 72.61,111.21 85.47,111.21C98.34,111.21 108.71,123.02 108.49,137.41C108.51,151.79 98.34,163.59 85.47,163.59ZM170.53,163.59C157.88,163.59 147.51,151.79 147.51,137.41C147.51,123.04 157.66,111.21 170.53,111.21C183.39,111.21 193.76,123.02 193.54,137.41C193.54,151.79 183.39,163.59 170.53,163.59Z"
android:fillColor="#5865F2" android:fillColor="#5865F2"
android:fillType="nonZero"/> android:fillType="nonZero"
android:pathData="M216.86,45.1C200.29,37.34 182.57,31.71 164.04,28.5C161.77,32.61 159.11,38.15 157.28,42.55C137.58,39.58 118.07,39.58 98.74,42.55C96.91,38.15 94.19,32.61 91.9,28.5C73.35,31.71 55.61,37.36 39.04,45.14C5.62,95.65 -3.44,144.9 1.09,193.46C23.26,210.01 44.74,220.07 65.86,226.65C71.08,219.47 75.73,211.84 79.74,203.8C72.1,200.9 64.79,197.32 57.89,193.17C59.72,191.81 61.51,190.39 63.24,188.93C105.37,208.63 151.13,208.63 192.75,188.93C194.51,190.39 196.3,191.81 198.11,193.17C191.18,197.34 183.85,200.92 176.22,203.82C180.23,211.84 184.86,219.49 190.1,226.67C211.24,220.09 232.74,210.03 254.91,193.46C260.23,137.17 245.83,88.37 216.86,45.1ZM85.47,163.59C72.83,163.59 62.46,151.79 62.46,137.41C62.46,123.04 72.61,111.21 85.47,111.21C98.34,111.21 108.71,123.02 108.49,137.41C108.51,151.79 98.34,163.59 85.47,163.59ZM170.53,163.59C157.88,163.59 147.51,151.79 147.51,137.41C147.51,123.04 157.66,111.21 170.53,111.21C183.39,111.21 193.76,123.02 193.54,137.41C193.54,151.79 183.39,163.59 170.53,163.59Z" />
</vector> </vector>

View File

@ -4,9 +4,9 @@
android:viewportWidth="20" android:viewportWidth="20"
android:viewportHeight="20"> android:viewportHeight="20">
<path <path
android:pathData="M10,0C15.523,0 20,4.59 20,10.253C20,14.782 17.138,18.624 13.167,19.981C12.66,20.082 12.48,19.762 12.48,19.489C12.48,19.151 12.492,18.047 12.492,16.675C12.492,15.719 12.172,15.095 11.813,14.777C14.04,14.523 16.38,13.656 16.38,9.718C16.38,8.598 15.992,7.684 15.35,6.966C15.454,6.707 15.797,5.664 15.252,4.252C15.252,4.252 14.414,3.977 12.505,5.303C11.706,5.076 10.85,4.962 10,4.958C9.15,4.962 8.295,5.076 7.497,5.303C5.586,3.977 4.746,4.252 4.746,4.252C4.203,5.664 4.546,6.707 4.649,6.966C4.01,7.684 3.619,8.598 3.619,9.718C3.619,13.646 5.954,14.526 8.175,14.785C7.889,15.041 7.63,15.493 7.54,16.156C6.97,16.418 5.522,16.871 4.63,15.304C4.63,15.304 4.101,14.319 3.097,14.247C3.097,14.247 2.122,14.234 3.029,14.87C3.029,14.87 3.684,15.185 4.139,16.37C4.139,16.37 4.726,18.2 7.508,17.58C7.513,18.437 7.522,19.245 7.522,19.489C7.522,19.76 7.338,20.077 6.839,19.982C2.865,18.627 0,14.783 0,10.253C0,4.59 4.478,0 10,0"
android:strokeWidth="1"
android:fillColor="#000000" android:fillColor="#000000"
android:fillType="evenOdd" android:fillType="evenOdd"
android: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" /> android:strokeColor="#00000000" />
</vector> </vector>

View File

@ -1,5 +1,10 @@
<vector android:height="24dp" android:tint="#000000" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:height="24dp"
<path android:fillColor="@android:color/white" android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12V5l-9,-4z"/> android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12V5l-9,-4z" />
</vector> </vector>

View File

@ -1,5 +1,10 @@
<vector android:height="24dp" android:tint="#000000" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:height="24dp"
<path android:fillColor="@android:color/white" android:pathData="M20.83,18H21v-4h2v-4H12.83L20.83,18zM19.78,22.61l1.41,-1.41L2.81,2.81L1.39,4.22l2.59,2.59C2.2,7.85 1,9.79 1,12c0,3.31 2.69,6 6,6c2.21,0 4.15,-1.2 5.18,-2.99L19.78,22.61zM8.99,11.82C9,11.88 9,11.94 9,12c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2s0.9,-2 2,-2c0.06,0 0.12,0 0.18,0.01L8.99,11.82z"/> android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M20.83,18H21v-4h2v-4H12.83L20.83,18zM19.78,22.61l1.41,-1.41L2.81,2.81L1.39,4.22l2.59,2.59C2.2,7.85 1,9.79 1,12c0,3.31 2.69,6 6,6c2.21,0 4.15,-1.2 5.18,-2.99L19.78,22.61zM8.99,11.82C9,11.88 9,11.94 9,12c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2s0.9,-2 2,-2c0.06,0 0.12,0 0.18,0.01L8.99,11.82z" />
</vector> </vector>

View File

@ -1,5 +1,10 @@
<vector android:height="24dp" android:tint="#000000" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:height="24dp"
<path android:fillColor="@android:color/white" android:pathData="M12.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z"/> android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z" />
</vector> </vector>

View File

@ -164,4 +164,7 @@
<string name="location_service_missing">Location Services Not Detected</string> <string name="location_service_missing">Location Services Not Detected</string>
<string name="location_services_missing_message">The app is not detecting any location services enabled on your device. Depending on the device, this could cause the untrusted wifi feature to fail to read the wifi name. Would you like to continue anyways?</string> <string name="location_services_missing_message">The app is not detecting any location services enabled on your device. Depending on the device, this could cause the untrusted wifi feature to fail to read the wifi name. Would you like to continue anyways?</string>
<string name="auto_tunnel_title">Auto-tunnel Service</string> <string name="auto_tunnel_title">Auto-tunnel Service</string>
<string name="delete_tunnel">Delete tunnel</string>
<string name="delete_tunnel_message">Are you sure you would like to delete this tunnel?</string>
<string name="yes">Yes</string>
</resources> </resources>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.WireguardAutoTunnel" parent="@style/Theme.AppCompat.NoActionBar"> <style name="Theme.WireguardAutoTunnel" parent="@style/Theme.AppCompat.NoActionBar">
<item name="android:windowBackground">@color/black_background</item> <item name="android:windowBackground">@color/black_background</item>
</style> </style>

View File

@ -1,31 +1,35 @@
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android"> <shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut <shortcut
android:shortcutId="defaultOn1"
android:enabled="true" android:enabled="true"
android:icon="@drawable/vpn_on" android:icon="@drawable/vpn_on"
android:shortcutShortLabel="@string/vpn_on" android:shortcutDisabledMessage="@string/vpn_on"
android:shortcutId="defaultOn1"
android:shortcutLongLabel="@string/default_vpn_on" android:shortcutLongLabel="@string/default_vpn_on"
android:shortcutDisabledMessage="@string/vpn_on"> android:shortcutShortLabel="@string/vpn_on">
<intent <intent
android:action="START" android:action="START"
android:targetPackage="com.zaneschepke.wireguardautotunnel" android:targetClass="com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsActivity"
android:targetClass="com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsActivity"> android:targetPackage="com.zaneschepke.wireguardautotunnel">
<extra android:name="className" android:value="WireGuardTunnelService" /> <extra
android:name="className"
android:value="WireGuardTunnelService" />
</intent> </intent>
<capability-binding android:key="actions.intent.START" /> <capability-binding android:key="actions.intent.START" />
</shortcut> </shortcut>
<shortcut <shortcut
android:shortcutId="defaultOff1"
android:enabled="true" android:enabled="true"
android:icon="@drawable/vpn_off" android:icon="@drawable/vpn_off"
android:shortcutShortLabel="@string/vpn_off" android:shortcutDisabledMessage="@string/vpn_off"
android:shortcutId="defaultOff1"
android:shortcutLongLabel="@string/default_vpn_off" android:shortcutLongLabel="@string/default_vpn_off"
android:shortcutDisabledMessage="@string/vpn_off"> android:shortcutShortLabel="@string/vpn_off">
<intent <intent
android:action="STOP" android:action="STOP"
android:targetPackage="com.zaneschepke.wireguardautotunnel" android:targetClass="com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsActivity"
android:targetClass="com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsActivity"> android:targetPackage="com.zaneschepke.wireguardautotunnel">
<extra android:name="className" android:value="WireGuardTunnelService" /> <extra
android:name="className"
android:value="WireGuardTunnelService" />
</intent> </intent>
<capability-binding android:key="actions.intent.STOP" /> <capability-binding android:key="actions.intent.STOP" />
</shortcut> </shortcut>

View File

@ -26,11 +26,9 @@ object BuildHelper {
} }
fun isReleaseBuild(gradle: Gradle): Boolean { fun isReleaseBuild(gradle: Gradle): Boolean {
return ( return (gradle.startParameter.taskNames.size > 0 &&
gradle.startParameter.taskNames.size > 0 &&
gradle.startParameter.taskNames[0].contains( gradle.startParameter.taskNames[0].contains(
"Release", "Release",
) ))
)
} }
} }

View File

@ -1,11 +1,13 @@
object Constants { object Constants {
const val VERSION_NAME = "3.3.2" const val VERSION_NAME = "3.3.3"
const val JVM_TARGET = "17" const val JVM_TARGET = "17"
const val VERSION_CODE = 33200 const val VERSION_CODE = 33300
const val TARGET_SDK = 34 const val TARGET_SDK = 34
const val MIN_SDK = 26 const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel" const val APP_ID = "com.zaneschepke.wireguardautotunnel"
const val APP_NAME = "wgtunnel" const val APP_NAME = "wgtunnel"
const val COMPOSE_COMPILER_EXTENSION_VERSION = "1.5.7"
const val STORE_PASS_VAR = "SIGNING_STORE_PASSWORD" const val STORE_PASS_VAR = "SIGNING_STORE_PASSWORD"
const val KEY_ALIAS_VAR = "SIGNING_KEY_ALIAS" const val KEY_ALIAS_VAR = "SIGNING_KEY_ALIAS"

View File

@ -5,13 +5,13 @@ platform :android do
desc "Deploy a beta version to the Google Play" desc "Deploy a beta version to the Google Play"
lane :beta do lane :beta do
gradle(task: "clean bundleGeneralRelease") gradle(task: "clean bundleGeneralRelease")
upload_to_play_store(track: 'beta') upload_to_play_store(track: 'beta', skip_upload_apk: true)
end end
desc "Deploy a new version to the Google Play" desc "Deploy a new version to the Google Play"
lane :production do lane :production do
gradle(task: "clean bundleGeneralRelease") gradle(task: "clean bundleGeneralRelease")
upload_to_play_store upload_to_play_store(skip_upload_apk: true)
end end
end end

View File

@ -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

View File

@ -11,7 +11,7 @@ desugar_jdk_libs = "2.0.4"
espressoCore = "3.5.1" espressoCore = "3.5.1"
firebase-crashlytics-gradle = "2.9.9" firebase-crashlytics-gradle = "2.9.9"
google-services = "4.4.0" google-services = "4.4.0"
hiltAndroid = "2.49" hiltAndroid = "2.50"
hiltNavigationCompose = "1.1.0" hiltNavigationCompose = "1.1.0"
junit = "4.13.2" junit = "4.13.2"
kotlinx-serialization-json = "1.6.2" kotlinx-serialization-json = "1.6.2"
@ -22,15 +22,14 @@ navigationCompose = "2.7.6"
roomVersion = "2.6.1" roomVersion = "2.6.1"
timber = "5.0.1" timber = "5.0.1"
tunnel = "1.0.20230706" tunnel = "1.0.20230706"
androidGradlePlugin = "8.2.0" androidGradlePlugin = "8.2.1"
kotlin="1.9.10" kotlin = "1.9.21"
ksp="1.9.10-1.0.13" ksp = "1.9.21-1.0.16"
composeBom = "2023.10.01" composeBom = "2023.10.01"
firebaseBom = "32.7.0" firebaseBom = "32.7.0"
compose = "1.5.4" compose = "1.5.4"
crashlytics = "18.6.0" crashlytics = "18.6.0"
analytics = "21.5.0" analytics = "21.5.0"
composeCompiler="1.5.3"
zxingAndroidEmbedded = "4.3.0" zxingAndroidEmbedded = "4.3.0"
zxingCore = "3.5.2" zxingCore = "3.5.2"
@ -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" } material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "material-icons-extended" }
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
tunnel = { module = "com.wireguard.android:tunnel", version.ref = "tunnel" } tunnel = { module = "com.wireguard.android:tunnel", version.ref = "tunnel" }
#firebase #firebase

View File

@ -5,6 +5,7 @@ pluginManagement {
gradlePluginPortal() gradlePluginPortal()
} }
} }
dependencyResolutionManagement { dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories { repositories {
@ -14,4 +15,5 @@ dependencyResolutionManagement {
} }
rootProject.name = "WG Tunnel" rootProject.name = "WG Tunnel"
include(":app") include(":app")