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.
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- Android Version: [e.g. iOS8.1]
- App Version [e.g. 22]
- Device: [e.g. Pixel 4a]
- Android Version: [e.g. Android 13]
- App Version [e.g. 3.3.3]
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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