diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..de4a9d8 --- /dev/null +++ b/.editorconfig @@ -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 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 3691c20..f406880 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -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 '....' diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md index ac02825..62eccbd 100644 --- a/.github/SUPPORT.md +++ b/.github/SUPPORT.md @@ -1,6 +1,6 @@ # Support -If you are experiencing issues with the app, the following resources are available to help you. +If you are experiencing issues with the app, the following resources are available to help you.
  1. diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index f81015e..6062e6b 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -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 diff --git a/README.md b/README.md index 2288905..6606f6a 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,10 @@ WG Tunnel
    -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.
    @@ -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,9 +67,8 @@ The original inspiration for this app came from the inconvenience of having to m * Automatic service restart after reboot * Battery preservation measures - ## Building - + ``` $ git clone https://github.com/zaneschepke/wgtunnel $ cd wgtunnel diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ae8e5ac..bf10316 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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,32 +43,41 @@ android { } } - // 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) + // 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), + ), ) - ) - storePassword = System.getenv().getOrDefault( - Constants.STORE_PASS_VAR, - properties.getProperty(Constants.STORE_PASS_VAR) - ) - keyAlias = System.getenv().getOrDefault( - Constants.KEY_ALIAS_VAR, - properties.getProperty(Constants.KEY_ALIAS_VAR) - ) - keyPassword = System.getenv().getOrDefault( - Constants.KEY_PASS_VAR, - properties.getProperty(Constants.KEY_PASS_VAR) - ) + storePassword = + System.getenv() + .getOrDefault( + Constants.STORE_PASS_VAR, + properties.getProperty(Constants.STORE_PASS_VAR), + ) + keyAlias = + System.getenv() + .getOrDefault( + Constants.KEY_ALIAS_VAR, + properties.getProperty(Constants.KEY_ALIAS_VAR), + ) + keyPassword = + System.getenv() + .getOrDefault( + Constants.KEY_PASS_VAR, + properties.getProperty(Constants.KEY_PASS_VAR), + ) } } 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) diff --git a/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/MigrationTest.kt b/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/MigrationTest.kt index 987e432..1ce565f 100644 --- a/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/MigrationTest.kt +++ b/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/MigrationTest.kt @@ -14,10 +14,11 @@ class MigrationTest { private val dbName = "migration-test" @get:Rule - val helper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java - ) + val helper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + ) @Test @Throws(IOException::class) @@ -27,34 +28,33 @@ class MigrationTest { // You can't use DAO classes because they expect the latest schema. execSQL( "INSERT INTO Settings (is_tunnel_enabled," + - "is_tunnel_on_mobile_data_enabled," + - "trusted_network_ssids," + - "default_tunnel," + - "is_always_on_vpn_enabled," + - "is_tunnel_on_ethernet_enabled," + - "is_shortcuts_enabled," + - "is_battery_saver_enabled," + - "is_tunnel_on_wifi_enabled," + - "is_kernel_enabled," + - "is_restore_on_boot_enabled," + - "is_multi_tunnel_enabled)" + - " VALUES " + - "('false'," + - "'false'," + - "'[trustedSSID1,trustedSSID2]'," + - "'defaultTunnel'," + - "'false'," + - "'false'," + - "'false'," + - "'false'," + - "'false'," + - "'false'," + - "'false'," + - "'false')" + "is_tunnel_on_mobile_data_enabled," + + "trusted_network_ssids," + + "default_tunnel," + + "is_always_on_vpn_enabled," + + "is_tunnel_on_ethernet_enabled," + + "is_shortcuts_enabled," + + "is_battery_saver_enabled," + + "is_tunnel_on_wifi_enabled," + + "is_kernel_enabled," + + "is_restore_on_boot_enabled," + + "is_multi_tunnel_enabled)" + + " VALUES " + + "('false'," + + "'false'," + + "'[trustedSSID1,trustedSSID2]'," + + "'defaultTunnel'," + + "'false'," + + "'false'," + + "'false'," + + "'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() diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5a31be9..de5b101 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,56 +1,63 @@ + - - - - - - - + + + + + - - - + + + + - + - - + android:name="android.hardware.screen.portrait" + android:required="false" /> + + - + + - + android:finishOnTaskLaunch="true" + android:theme="@android:style/Theme.NoDisplay" /> + - + tools:node="merge" /> - - + + android:permission="android.permission.BIND_VPN_SERVICE" + android:persistent="true" + tools:node="merge"> - + - - - + + + - + \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt index 7b4288a..3b6507c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt @@ -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), + ) } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt index 666f813..57934e6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt @@ -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( - from = 3, - to = 4 - ),AutoMigration( - from = 4, - to = 5 - ) - ], - exportSchema = true + autoMigrations = + [ + AutoMigration(from = 1, to = 2), + AutoMigration(from = 2, to = 3), + AutoMigration( + from = 3, + to = 4, + ), + AutoMigration( + from = 4, + to = 5, + ), + ], + exportSchema = true, ) @TypeConverters(DatabaseListConverters::class) abstract class AppDatabase : RoomDatabase() { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/SettingsDao.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/SettingsDao.kt index ffd1574..77061ae 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/SettingsDao.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/SettingsDao.kt @@ -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) + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List) - @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 + @Query("SELECT * FROM settings") suspend fun getAll(): List - @Query("SELECT * FROM settings LIMIT 1") - fun getSettingsFlow(): Flow + @Query("SELECT * FROM settings LIMIT 1") fun getSettingsFlow(): Flow - @Query("SELECT * FROM settings") - fun getAllFlow(): Flow> + @Query("SELECT * FROM settings") fun getAllFlow(): Flow> - @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 } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/TunnelConfigDao.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/TunnelConfigDao.kt index 8460ace..930c261 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/TunnelConfigDao.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/TunnelConfigDao.kt @@ -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) + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List) - @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 + @Query("SELECT * FROM TunnelConfig") suspend fun getAll(): List - @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> + @Query("SELECT * FROM tunnelconfig") fun getAllFlow(): Flow> } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/DataStoreManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/DataStoreManager.kt index 6d9c67c..8af0239 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/DataStoreManager.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/DataStoreManager.kt @@ -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,29 +12,27 @@ 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() { context.dataStore.data.first() } suspend fun saveToDataStore(key: Preferences.Key, value: T) = - context.dataStore.edit { - it[key] = value - } - fun getFromStoreFlow(key: Preferences.Key) = context.dataStore.data.map { - it[key] - } + context.dataStore.edit { it[key] = value } - suspend fun getFromStore(key: Preferences.Key) = context.dataStore.data.first { it.contains(key) }[key] + fun getFromStoreFlow(key: Preferences.Key) = context.dataStore.data.map { it[key] } - val locationDisclosureFlow: Flow = context.dataStore.data.map { - it[LOCATION_DISCLOSURE_SHOWN] - } + suspend fun getFromStore(key: Preferences.Key) = + context.dataStore.data.first { it.contains(key) }[key] + + val preferencesFlow: Flow = context.dataStore.data } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/Settings.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/Settings.kt index d92c74e..4aa3789 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/Settings.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/Settings.kt @@ -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 = mutableListOf(), + @ColumnInfo(name = "is_tunnel_on_mobile_data_enabled") + var isTunnelOnMobileDataEnabled: Boolean = false, + @ColumnInfo(name = "trusted_network_ssids") + var trustedNetworkSSIDs: MutableList = 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, + name = "is_restore_on_boot_enabled", + defaultValue = "false", + ) + var isRestoreOnBootEnabled: Boolean = false, @ColumnInfo( - name = "is_multi_tunnel_enabled", - defaultValue = "false" - ) var isMultiTunnelEnabled: Boolean = false, + name = "is_multi_tunnel_enabled", + 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) { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/SettingsRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/SettingsRepository.kt index 685fe5b..2f27f90 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/SettingsRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/SettingsRepository.kt @@ -4,9 +4,11 @@ import com.zaneschepke.wireguardautotunnel.data.model.Settings import kotlinx.coroutines.flow.Flow interface SettingsRepository { - suspend fun save(settings : Settings) - fun getSettingsFlow() : Flow + suspend fun save(settings: Settings) - suspend fun getSettings() : Settings - suspend fun getAll() : List -} \ No newline at end of file + fun getSettingsFlow(): Flow + + suspend fun getSettings(): Settings + + suspend fun getAll(): List +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/SettingsRepositoryImpl.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/SettingsRepositoryImpl.kt index 2c47662..abdadd9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/SettingsRepositoryImpl.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/SettingsRepositoryImpl.kt @@ -21,4 +21,4 @@ class SettingsRepositoryImpl(private val settingsDoa: SettingsDao) : SettingsRep override suspend fun getAll(): List { return settingsDoa.getAll() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepository.kt index f0c094d..1a12215 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepository.kt @@ -6,9 +6,13 @@ import kotlinx.coroutines.flow.Flow interface TunnelConfigRepository { - fun getTunnelConfigsFlow() : Flow - suspend fun getAll() : TunnelConfigs + fun getTunnelConfigsFlow(): Flow + + suspend fun getAll(): TunnelConfigs + suspend fun save(tunnelConfig: TunnelConfig) + suspend fun delete(tunnelConfig: TunnelConfig) - suspend fun count() : Int -} \ No newline at end of file + + suspend fun count(): Int +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepositoryImpl.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepositoryImpl.kt index f9e3be7..b681ac1 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepositoryImpl.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepositoryImpl.kt @@ -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 { return tunnelConfigDao.getAllFlow() } @@ -25,4 +26,4 @@ class TunnelConfigRepositoryImpl(private val tunnelConfigDao: TunnelConfigDao) : override suspend fun count(): Int { return tunnelConfigDao.count().toInt() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/DatabaseModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/DatabaseModule.kt index 4634ecb..3ca9695 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/DatabaseModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/DatabaseModule.kt @@ -16,14 +16,12 @@ 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, + AppDatabase::class.java, + context.getString(R.string.db_name), + ) .fallbackToDestructiveMigration() .build() } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/Kernel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/Kernel.kt index a763c09..c4b0d2d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/Kernel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/Kernel.kt @@ -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 diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ServiceModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ServiceModule.kt index dfa9510..3a438b9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ServiceModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ServiceModule.kt @@ -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 + abstract fun provideMobileDataService( + mobileDataService: MobileDataService + ): NetworkService @Binds @ServiceScoped - abstract fun provideEthernetService(ethernetService: EthernetService): NetworkService + abstract fun provideEthernetService( + ethernetService: EthernetService + ): NetworkService } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt index d274ad0..bb490f6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt @@ -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)) } @@ -51,7 +44,7 @@ class TunnelModule { fun provideVpnService( @Userspace userspaceBackend: Backend, @Kernel kernelBackend: Backend, - settingsRepository : SettingsRepository + settingsRepository: SettingsRepository ): VpnService { return WireGuardTunnel(userspaceBackend, kernelBackend, settingsRepository) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/Userspace.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/Userspace.kt index 8a85a7d..f064aab 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/Userspace.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/Userspace.kt @@ -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 diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt index 288e84d..3092864 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt @@ -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) { + if (settingsRepository.getSettings().isAutoTunnelEnabled) { ServiceManager.startWatcherServiceForeground(context!!) } } - } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt index dfbd4c2..4ba46b6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt @@ -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) { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ForegroundService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ForegroundService.kt index 0814e91..9e3c8bb 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ForegroundService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ForegroundService.kt @@ -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 diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt index d6b2224..b29202d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt @@ -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 Context.isServiceRunning(service: Class) = - (getSystemService(ACTIVITY_SERVICE) as ActivityManager) - .getRunningServices(Integer.MAX_VALUE) - .any { it.service.className == service.name } - fun getServiceState( - context: Context, - cls: Class - ): ServiceState { - val isServiceRunning = context.isServiceRunning(cls) - return if (isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED - } + // private + // fun Context.isServiceRunning(service: Class) = + // (getSystemService(ACTIVITY_SERVICE) as ActivityManager) + // .runningAppProcesses.any { + // it.processName == service.name + // } + // + // fun getServiceState( + // context: Context, + // cls: Class + // ): ServiceState { + // val isServiceRunning = context.isServiceRunning(cls) + // return if (isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED + // } private fun actionOnService( action: Action, @@ -30,14 +29,10 @@ object ServiceManager { cls: Class, extras: Map? = 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, ) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt index 715c7ef..b6f62f6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt @@ -34,334 +34,361 @@ import javax.inject.Inject @AndroidEntryPoint class WireGuardConnectivityWatcherService : ForegroundService() { - private val foregroundId = 122 + private val foregroundId = 122 - @Inject lateinit var wifiService: NetworkService + @Inject lateinit var wifiService: NetworkService - @Inject lateinit var mobileDataService: NetworkService + @Inject lateinit var mobileDataService: NetworkService - @Inject lateinit var ethernetService: NetworkService + @Inject lateinit var ethernetService: NetworkService - @Inject lateinit var settingsRepository: SettingsRepository + @Inject lateinit var settingsRepository: SettingsRepository - @Inject lateinit var notificationService: NotificationService + @Inject lateinit var notificationService: NotificationService - @Inject lateinit var vpnService: VpnService + @Inject lateinit var vpnService: VpnService - private val networkEventsFlow = MutableStateFlow(WatcherState()) - data class WatcherState( - val isWifiConnected: Boolean = false, - val isVpnConnected : Boolean = false, - val isEthernetConnected: Boolean = false, - val isMobileDataConnected: Boolean = false, - val currentNetworkSSID: String = "", - val settings: Settings = Settings() - ) + private val networkEventsFlow = MutableStateFlow(WatcherState()) - private lateinit var watcherJob: Job + data class WatcherState( + val isWifiConnected: Boolean = false, + val isVpnConnected: Boolean = false, + val isEthernetConnected: Boolean = false, + val isMobileDataConnected: Boolean = false, + val currentNetworkSSID: String = "", + val settings: Settings = Settings() + ) - private var wakeLock: PowerManager.WakeLock? = null - private val tag = this.javaClass.name + private lateinit var watcherJob: Job - override fun onCreate() { - super.onCreate() - lifecycleScope.launch(Dispatchers.Main) { - try { - if(settingsRepository.getSettings().isAutoTunnelPaused) { - launchWatcherPausedNotification() - } else launchWatcherNotification() - } catch (e: Exception) { - Timber.e("Failed to start watcher service, not enough permissions") - } + private var wakeLock: PowerManager.WakeLock? = null + private val tag = this.javaClass.name + + override fun onCreate() { + super.onCreate() + lifecycleScope.launch(Dispatchers.Main) { + try { + if (settingsRepository.getSettings().isAutoTunnelPaused) { + launchWatcherPausedNotification() + } else launchWatcherNotification() + } catch (e: Exception) { + Timber.e("Failed to start watcher service, not enough permissions") + } + } } - } - override fun startService(extras: Bundle?) { - super.startService(extras) - try { - // we need this lock so our service gets not affected by Doze Mode - lifecycleScope.launch { initWakeLock() } - cancelWatcherJob() - startWatcherJob() - } catch (e: Exception) { - Timber.e("Failed to launch watcher service, no permissions") + override fun startService(extras: Bundle?) { + super.startService(extras) + try { + // we need this lock so our service gets not affected by Doze Mode + lifecycleScope.launch { initWakeLock() } + cancelWatcherJob() + startWatcherJob() + } catch (e: Exception) { + Timber.e("Failed to launch watcher service, no permissions") + } } - } - override fun stopService(extras: Bundle?) { - super.stopService(extras) - wakeLock?.let { - if (it.isHeld) { - it.release() - } + override fun stopService(extras: Bundle?) { + super.stopService(extras) + wakeLock?.let { + if (it.isHeld) { + it.release() + } + } + cancelWatcherJob() + stopSelf() } - cancelWatcherJob() - stopSelf() - } - 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) - ServiceCompat.startForeground( - this, foregroundId, notification, Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID) - } + 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, + ) + ServiceCompat.startForeground( + this, + foregroundId, + notification, + Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID, + ) + } private fun launchWatcherPausedNotification() { launchWatcherNotification(getString(R.string.watcher_notification_text_paused)) } - // TODO could this be restarting service in a bad state? - // try to start task again if killed - override fun onTaskRemoved(rootIntent: Intent) { - Timber.d("Task Removed called") - val restartServiceIntent = Intent(rootIntent) - val restartServicePendingIntent: PendingIntent = - PendingIntent.getService( - this, - 1, - restartServiceIntent, - 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) - } - - private suspend fun initWakeLock() { - val isBatterySaverOn = - withContext(lifecycleScope.coroutineContext) { - settingsRepository.getSettings().isBatterySaverEnabled - } - wakeLock = - (getSystemService(Context.POWER_SERVICE) as PowerManager).run { - newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply { - if (isBatterySaverOn) { - Timber.d("Initiating wakelock with timeout") - acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT) - } else { - Timber.d("Initiating wakelock with zero timeout") - acquire(Constants.DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT) - } - } - } - } - - private fun cancelWatcherJob() { - if (this::watcherJob.isInitialized) { - watcherJob.cancel() + // TODO could this be restarting service in a bad state? + // try to start task again if killed + override fun onTaskRemoved(rootIntent: Intent) { + Timber.d("Task Removed called") + val restartServiceIntent = Intent(rootIntent) + val restartServicePendingIntent: PendingIntent = + PendingIntent.getService( + this, + 1, + restartServiceIntent, + 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, + ) } - } - private fun startWatcherJob() { - watcherJob = - lifecycleScope.launch(Dispatchers.IO) { - val setting = settingsRepository.getSettings() - launch { - Timber.d("Starting wifi watcher") - watchForWifiConnectivityChanges() - } - if (setting.isTunnelOnMobileDataEnabled) { - launch { - Timber.d("Starting mobile data watcher") - watchForMobileDataConnectivityChanges() + private suspend fun initWakeLock() { + val isBatterySaverOn = + withContext(lifecycleScope.coroutineContext) { + settingsRepository.getSettings().isBatterySaverEnabled } - } - if (setting.isTunnelOnEthernetEnabled) { - launch { - Timber.d("Starting ethernet data watcher") - watchForEthernetConnectivityChanges() + wakeLock = + (getSystemService(Context.POWER_SERVICE) as PowerManager).run { + newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply { + if (isBatterySaverOn) { + Timber.d("Initiating wakelock with timeout") + acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT) + } else { + Timber.d("Initiating wakelock with zero timeout") + acquire(Constants.DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT) + } + } } - } - launch { - Timber.d("Starting vpn state watcher") - watchForVpnConnectivityChanges() - } - launch { - Timber.d("Starting settings watcher") - watchForSettingsChanges() - } - launch { - Timber.d("Starting management watcher") - manageVpn() - } - } - } - - private suspend fun watchForMobileDataConnectivityChanges() { - mobileDataService.networkStatus.collect { - when (it) { - is NetworkStatus.Available -> { - Timber.d("Gained Mobile data connection") - networkEventsFlow.value = networkEventsFlow.value.copy( - isMobileDataConnected = true - ) - } - is NetworkStatus.CapabilitiesChanged -> { - networkEventsFlow.value = networkEventsFlow.value.copy( - isMobileDataConnected = true - ) - Timber.d("Mobile data capabilities changed") - } - is NetworkStatus.Unavailable -> { - networkEventsFlow.value = networkEventsFlow.value.copy( - isMobileDataConnected = false - ) - Timber.d("Lost mobile data connection") - } - } } - } + + private fun cancelWatcherJob() { + if (this::watcherJob.isInitialized) { + watcherJob.cancel() + } + } + + private fun startWatcherJob() { + watcherJob = + lifecycleScope.launch(Dispatchers.IO) { + val setting = settingsRepository.getSettings() + launch { + Timber.d("Starting wifi watcher") + watchForWifiConnectivityChanges() + } + if (setting.isTunnelOnMobileDataEnabled) { + launch { + Timber.d("Starting mobile data watcher") + watchForMobileDataConnectivityChanges() + } + } + if (setting.isTunnelOnEthernetEnabled) { + launch { + Timber.d("Starting ethernet data watcher") + watchForEthernetConnectivityChanges() + } + } + launch { + Timber.d("Starting vpn state watcher") + watchForVpnConnectivityChanges() + } + launch { + Timber.d("Starting settings watcher") + watchForSettingsChanges() + } + launch { + Timber.d("Starting management watcher") + manageVpn() + } + } + } + + private suspend fun watchForMobileDataConnectivityChanges() { + mobileDataService.networkStatus.collect { + when (it) { + is NetworkStatus.Available -> { + Timber.d("Gained Mobile data connection") + networkEventsFlow.value = + networkEventsFlow.value.copy( + isMobileDataConnected = true, + ) + } + is NetworkStatus.CapabilitiesChanged -> { + networkEventsFlow.value = + networkEventsFlow.value.copy( + isMobileDataConnected = true, + ) + Timber.d("Mobile data capabilities changed") + } + is NetworkStatus.Unavailable -> { + networkEventsFlow.value = + networkEventsFlow.value.copy( + isMobileDataConnected = false, + ) + Timber.d("Lost mobile data connection") + } + } + } + } + private suspend fun watchForSettingsChanges() { settingsRepository.getSettingsFlow().collect { - if(networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) { - when(it.isAutoTunnelPaused) { + if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) { + when (it.isAutoTunnelPaused) { true -> launchWatcherPausedNotification() false -> launchWatcherNotification() } } - networkEventsFlow.value = networkEventsFlow.value.copy( - settings = it - ) + networkEventsFlow.value = + networkEventsFlow.value.copy( + settings = it, + ) } } private suspend fun watchForVpnConnectivityChanges() { vpnService.vpnState.collect { - when(it.status) { - Tunnel.State.DOWN -> networkEventsFlow.value = networkEventsFlow.value.copy( - isVpnConnected = false - ) - Tunnel.State.UP -> networkEventsFlow.value = networkEventsFlow.value.copy( - isVpnConnected = true - ) + when (it.status) { + Tunnel.State.DOWN -> + networkEventsFlow.value = + networkEventsFlow.value.copy( + isVpnConnected = false, + ) + Tunnel.State.UP -> + networkEventsFlow.value = + networkEventsFlow.value.copy( + isVpnConnected = true, + ) else -> {} } } } - private suspend fun watchForEthernetConnectivityChanges() { - ethernetService.networkStatus.collect { - when (it) { - is NetworkStatus.Available -> { - Timber.d("Gained Ethernet connection") - networkEventsFlow.value = networkEventsFlow.value.copy( - isEthernetConnected = true - ) - } - is NetworkStatus.CapabilitiesChanged -> { - Timber.d("Ethernet capabilities changed") - networkEventsFlow.value = networkEventsFlow.value.copy( - isEthernetConnected = true - ) - } - is NetworkStatus.Unavailable -> { - networkEventsFlow.value = networkEventsFlow.value.copy( - isEthernetConnected = false - ) - Timber.d("Lost Ethernet connection") - } - } - } - } - - private suspend fun watchForWifiConnectivityChanges() { - wifiService.networkStatus.collect { - when (it) { - is NetworkStatus.Available -> { - Timber.d("Gained Wi-Fi connection") - networkEventsFlow.value = networkEventsFlow.value.copy( - isWifiConnected = true - ) - } - is NetworkStatus.CapabilitiesChanged -> { - Timber.d("Wifi capabilities changed") - networkEventsFlow.value = networkEventsFlow.value.copy( - isWifiConnected = true - ) - val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: "" - Timber.d("Detected SSID: $ssid") - networkEventsFlow.value = networkEventsFlow.value.copy( - currentNetworkSSID = ssid - ) - } - is NetworkStatus.Unavailable -> { - networkEventsFlow.value = networkEventsFlow.value.copy( - isWifiConnected = false - ) - Timber.d("Lost Wi-Fi connection") - } - } - } - } - - //TODO clean this up - private suspend fun manageVpn() { - networkEventsFlow.collectLatest { - Timber.i("New watcher state: $it") - if (!it.settings.isAutoTunnelPaused && it.settings.defaultTunnel != null) { - delay(Constants.TOGGLE_TUNNEL_DELAY) - when { - ((it.isEthernetConnected && - it.settings.isTunnelOnEthernetEnabled && - !it.isVpnConnected)) -> { - ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!) - Timber.i("Condition 1 met") + private suspend fun watchForEthernetConnectivityChanges() { + ethernetService.networkStatus.collect { + when (it) { + is NetworkStatus.Available -> { + Timber.d("Gained Ethernet connection") + networkEventsFlow.value = + networkEventsFlow.value.copy( + isEthernetConnected = true, + ) } - (!it.isEthernetConnected && - it.settings.isTunnelOnMobileDataEnabled && - !it.isWifiConnected && - it.isMobileDataConnected && - !it.isVpnConnected) -> { - ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!) - Timber.i("Condition 2 met") + is NetworkStatus.CapabilitiesChanged -> { + Timber.d("Ethernet capabilities changed") + networkEventsFlow.value = + networkEventsFlow.value.copy( + isEthernetConnected = true, + ) } - (!it.isEthernetConnected && - !it.settings.isTunnelOnMobileDataEnabled && - !it.isWifiConnected && - it.isVpnConnected) -> { - ServiceManager.stopVpnService(this) - Timber.i("Condition 3 met") - } - (!it.isEthernetConnected && - it.isWifiConnected && - !it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID) && - it.settings.isTunnelOnWifiEnabled && - (!it.isVpnConnected)) -> { - ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!) - Timber.i("Condition 4 met") - } - (!it.isEthernetConnected && - (it.isWifiConnected && it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) && - (it.isVpnConnected)) -> { - ServiceManager.stopVpnService(this) - Timber.i("Condition 5 met") - } - (!it.isEthernetConnected && - (it.isWifiConnected && - !it.settings.isTunnelOnWifiEnabled && - (it.isVpnConnected))) -> { - ServiceManager.stopVpnService(this) - Timber.i("Condition 6 met") - } - (!it.isEthernetConnected && - !it.isWifiConnected && - !it.isMobileDataConnected && - (it.isVpnConnected)) -> { - ServiceManager.stopVpnService(this) - Timber.i("Condition 7 met") - } - else -> { - Timber.i("No condition met") + is NetworkStatus.Unavailable -> { + networkEventsFlow.value = + networkEventsFlow.value.copy( + isEthernetConnected = false, + ) + Timber.d("Lost Ethernet connection") + } + } + } + } + + private suspend fun watchForWifiConnectivityChanges() { + wifiService.networkStatus.collect { + when (it) { + is NetworkStatus.Available -> { + Timber.d("Gained Wi-Fi connection") + networkEventsFlow.value = + networkEventsFlow.value.copy( + isWifiConnected = true, + ) + } + is NetworkStatus.CapabilitiesChanged -> { + Timber.d("Wifi capabilities changed") + networkEventsFlow.value = + networkEventsFlow.value.copy( + isWifiConnected = true, + ) + val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: "" + Timber.d("Detected SSID: $ssid") + networkEventsFlow.value = + networkEventsFlow.value.copy( + currentNetworkSSID = ssid, + ) + } + is NetworkStatus.Unavailable -> { + networkEventsFlow.value = + networkEventsFlow.value.copy( + isWifiConnected = false, + ) + Timber.d("Lost Wi-Fi connection") + } + } + } + } + + // TODO clean this up + private suspend fun manageVpn() { + networkEventsFlow.collectLatest { + Timber.i("New watcher state: $it") + if (!it.settings.isAutoTunnelPaused && it.settings.defaultTunnel != null) { + delay(Constants.TOGGLE_TUNNEL_DELAY) + when { + ((it.isEthernetConnected && + it.settings.isTunnelOnEthernetEnabled && + !it.isVpnConnected)) -> { + ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!) + Timber.i("Condition 1 met") + } + (!it.isEthernetConnected && + it.settings.isTunnelOnMobileDataEnabled && + !it.isWifiConnected && + it.isMobileDataConnected && + !it.isVpnConnected) -> { + ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!) + Timber.i("Condition 2 met") + } + (!it.isEthernetConnected && + !it.settings.isTunnelOnMobileDataEnabled && + !it.isWifiConnected && + it.isVpnConnected) -> { + ServiceManager.stopVpnService(this) + Timber.i("Condition 3 met") + } + (!it.isEthernetConnected && + it.isWifiConnected && + !it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID) && + it.settings.isTunnelOnWifiEnabled && + (!it.isVpnConnected)) -> { + ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!) + Timber.i("Condition 4 met") + } + (!it.isEthernetConnected && + (it.isWifiConnected && + it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) && + (it.isVpnConnected)) -> { + ServiceManager.stopVpnService(this) + Timber.i("Condition 5 met") + } + (!it.isEthernetConnected && + (it.isWifiConnected && + !it.settings.isTunnelOnWifiEnabled && + (it.isVpnConnected))) -> { + ServiceManager.stopVpnService(this) + Timber.i("Condition 6 met") + } + (!it.isEthernetConnected && + !it.isWifiConnected && + !it.isMobileDataConnected && + (it.isVpnConnected)) -> { + ServiceManager.stopVpnService(this) + Timber.i("Condition 7 met") + } + else -> { + Timber.i("No condition met") + } } } } } - } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt index 62641b9..f4501ac 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt @@ -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 @@ -48,7 +44,7 @@ class WireGuardTunnelService : ForegroundService() { override fun onCreate() { super.onCreate() lifecycleScope.launch(Dispatchers.Main) { - if(tunnelConfigRepository.getAll().isNotEmpty()) { + if (tunnelConfigRepository.getAll().isNotEmpty()) { launchVpnNotification() } } @@ -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,22 +72,22 @@ class WireGuardTunnelService : ForegroundService() { val settings = settingsRepository.getSettings() val tunnels = tunnelConfigRepository.getAll() if (settings.isAlwaysOnVpnEnabled) { - val tunnel = if(settings.defaultTunnel != null) { - TunnelConfig.from(settings.defaultTunnel!!) - } else if(tunnels.isNotEmpty()) { - tunnels.first() - } else { - null - } - if(tunnel != null) { + val tunnel = + if (settings.defaultTunnel != null) { + TunnelConfig.from(settings.defaultTunnel!!) + } else if (tunnels.isNotEmpty()) { + tunnels.first() + } else { + null + } + if (tunnel != null) { tunnelName = tunnel.name vpnService.startTunnel(tunnel) } - } } } - //TODO add failed to connect notification + // TODO add failed to connect notification launch { vpnService.vpnState.collect { state -> state.statistics @@ -101,14 +96,18 @@ class WireGuardTunnelService : ForegroundService() { .let { statuses -> when { statuses?.all { it == HandshakeStatus.HEALTHY } == true -> { - if(!didShowConnected){ + 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, ) } @@ -152,24 +154,24 @@ class WireGuardTunnelService : ForegroundService() { channelId = getString(R.string.vpn_channel_id), channelName = getString(R.string.vpn_channel_name), action = - PendingIntent.getBroadcast( - this, - 0, - Intent(this, NotificationActionReceiver::class.java), - PendingIntent.FLAG_IMMUTABLE - ), + PendingIntent.getBroadcast( + this, + 0, + Intent(this, NotificationActionReceiver::class.java), + 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, ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/BaseNetworkService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/BaseNetworkService.kt index 4f5e416..56be188 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/BaseNetworkService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/BaseNetworkService.kt @@ -24,72 +24,69 @@ abstract class BaseNetworkService>( private val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager - 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 + 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, ) { - override fun onAvailable(network: Network) { - trySend(NetworkStatus.Available(network)) - } - - override fun onLost(network: Network) { - trySend(NetworkStatus.Unavailable(network)) - } - - override fun onCapabilitiesChanged( - network: Network, - networkCapabilities: NetworkCapabilities - ) { - trySend( - NetworkStatus.CapabilitiesChanged( - network, - networkCapabilities - ) - ) - } + override fun onAvailable(network: Network) { + trySend(NetworkStatus.Available(network)) } - } - else -> { - object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - trySend(NetworkStatus.Available(network)) - } + override fun onLost(network: Network) { + trySend(NetworkStatus.Unavailable(network)) + } - override fun onLost(network: Network) { - trySend(NetworkStatus.Unavailable(network)) - } - - override fun onCapabilitiesChanged( - network: Network, - networkCapabilities: NetworkCapabilities - ) { - trySend( - NetworkStatus.CapabilitiesChanged( - network, - networkCapabilities - ) - ) - } + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities + ) { + trySend( + NetworkStatus.CapabilitiesChanged( + network, + networkCapabilities, + ), + ) } } } - val request = - NetworkRequest.Builder() - .addTransportType(networkCapability) - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) - .build() - connectivityManager.registerNetworkCallback(request, networkStatusCallback) + else -> { + object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + trySend(NetworkStatus.Available(network)) + } - awaitClose { - connectivityManager.unregisterNetworkCallback(networkStatusCallback) + override fun onLost(network: Network) { + trySend(NetworkStatus.Unavailable(network)) + } + + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities + ) { + trySend( + NetworkStatus.CapabilitiesChanged( + network, + networkCapabilities, + ), + ) + } + } + } } - } + val request = + NetworkRequest.Builder() + .addTransportType(networkCapability) + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + .build() + connectivityManager.registerNetworkCallback(request, networkStatusCallback) + + awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) } + } override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? { var ssid: String? = getWifiNameFromCapabilities(networkCapabilities) @@ -119,18 +116,16 @@ abstract class BaseNetworkService>( inline fun Flow.map( crossinline onUnavailable: suspend (network: Network) -> Result, crossinline onAvailable: suspend (network: Network) -> Result, - crossinline onCapabilitiesChanged: suspend ( - network: Network, - networkCapabilities: NetworkCapabilities - ) -> Result -): Flow = - map { status -> - when (status) { - is NetworkStatus.Unavailable -> onUnavailable(status.network) - is NetworkStatus.Available -> onAvailable(status.network) - is NetworkStatus.CapabilitiesChanged -> onCapabilitiesChanged( + crossinline onCapabilitiesChanged: + suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result +): Flow = map { status -> + when (status) { + is NetworkStatus.Unavailable -> onUnavailable(status.network) + is NetworkStatus.Available -> onAvailable(status.network) + is NetworkStatus.CapabilitiesChanged -> + onCapabilitiesChanged( status.network, - status.networkCapabilities + status.networkCapabilities, ) - } } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/EthernetService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/EthernetService.kt index 1f86836..36f76eb 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/EthernetService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/EthernetService.kt @@ -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(context, NetworkCapabilities.TRANSPORT_ETHERNET) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/MobileDataService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/MobileDataService.kt index a488e3a..626ca4e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/MobileDataService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/MobileDataService.kt @@ -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(context, NetworkCapabilities.TRANSPORT_CELLULAR) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/WifiService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/WifiService.kt index ebc4797..aafccb4 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/WifiService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/WifiService.kt @@ -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(context, NetworkCapabilities.TRANSPORT_WIFI) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/NotificationService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/NotificationService.kt index 005f1e6..cabd17b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/NotificationService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/NotificationService.kt @@ -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( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/WireGuardNotification.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/WireGuardNotification.kt index 3180319..8a8cfbf 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/WireGuardNotification.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/WireGuardNotification.kt @@ -13,23 +13,21 @@ 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( + context, + context.getString(R.string.vpn_channel_id), ) - private val tunnelBuilder: NotificationCompat.Builder = NotificationCompat.Builder( - context, - context.getString(R.string.vpn_channel_id) - ) override fun createNotification( channelId: String, @@ -47,17 +45,18 @@ constructor( ): Notification { val channel = NotificationChannel( - channelId, - channelName, - importance - ).let { - it.description = title - it.enableLights(lights) - it.lightColor = Color.RED - it.enableVibration(vibration) - it.vibrationPattern = longArrayOf(100,200,300) - it - } + channelId, + channelName, + importance, + ) + .let { + it.description = title + it.enableLights(lights) + it.lightColor = Color.RED + it.enableVibration(vibration) + it.vibrationPattern = longArrayOf(100, 200, 300) + it + } notificationManager.createNotificationChannel(channel) val pendingIntent: PendingIntent = Intent(context, MainActivity::class.java).let { notificationIntent -> @@ -65,26 +64,26 @@ constructor( context, 0, notificationIntent, - PendingIntent.FLAG_IMMUTABLE + PendingIntent.FLAG_IMMUTABLE, ) } - 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 - ) + 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, + ) + } } - } 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) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt index 6ab9c78..f5d1e0f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt @@ -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 - )) - } + val settings = settingsRepository.getSettings() + if (settings.isAutoTunnelEnabled) { + val pauseAutoTunnel = !settings.isAutoTunnelPaused + 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,13 +62,15 @@ class ShortcutsActivity : ComponentActivity() { tunnelConfig ?: return@launch toggleWatcherServicePause() when (intent.action) { - Action.STOP.name -> ServiceManager.stopVpnService( - this@ShortcutsActivity - ) - Action.START.name -> ServiceManager.startVpnServiceForeground( - this@ShortcutsActivity, - tunnelConfig.toString() - ) + Action.STOP.name -> + ServiceManager.stopVpnService( + this@ShortcutsActivity, + ) + Action.START.name -> + ServiceManager.startVpnServiceForeground( + this@ShortcutsActivity, + tunnelConfig.toString(), + ) } } catch (e: Exception) { Timber.e(e.message) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt index efc4a9d..e385a90 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt @@ -20,44 +20,43 @@ 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) - private var tunnelName : String? = null + private var tunnelName: String? = null override fun onStartListening() { super.onStartListening() Timber.d("On start listening called") scope.launch { vpnService.vpnState.collect { - when(it.status) { + when (it.status) { Tunnel.State.UP -> setActive() Tunnel.State.DOWN -> setInactive() else -> setInactive() } val tunnels = tunnelConfigRepository.getAll() - if(tunnels.isEmpty()) { + if (tunnels.isEmpty()) { setUnavailable() return@collect } - tunnelName = it.name.ifBlank { - val settings = settingsRepository.getSettings() - if (settings.defaultTunnel != null) { - TunnelConfig.from(settings.defaultTunnel!!).name - } else tunnels.firstOrNull()?.name - } + tunnelName = + it.name.ifBlank { + val settings = settingsRepository.getSettings() + if (settings.defaultTunnel != null) { + TunnelConfig.from(settings.defaultTunnel!!).name + } else tunnels.firstOrNull()?.name + } setTileDescription(tunnelName ?: "") } } } + 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, + ), + ) } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/HandshakeStatus.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/HandshakeStatus.kt index 87621cc..13bd8a7 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/HandshakeStatus.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/HandshakeStatus.kt @@ -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 } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnState.kt index 077f7ad..730238c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnState.kt @@ -4,7 +4,7 @@ import com.wireguard.android.backend.Statistics import com.wireguard.android.backend.Tunnel data class VpnState( - val status : Tunnel.State = Tunnel.State.DOWN, - val name : String = "", - val statistics : Statistics? = null + val status: Tunnel.State = Tunnel.State.DOWN, + val name: String = "", + val statistics: Statistics? = null ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt index 53c039c..a6974f9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt @@ -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, + ), ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/ActivityViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/ActivityViewModel.kt index 375611c..bee1b9b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/ActivityViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/ActivityViewModel.kt @@ -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() {} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt index d4a0ca2..6b83a61 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt @@ -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,19 +54,40 @@ 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() + // val activityViewModel = hiltViewModel() val navController = rememberNavController() - val focusRequester = remember { FocusRequester()} + val focusRequester = remember { FocusRequester() } WireguardAutoTunnelTheme { TransparentSystemBars() @@ -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( - Screen.Main.navItem, - Screen.Settings.navItem, - Screen.Support.navItem)) } - } else { - {} - } + if (vpnIntent == null && notificationPermissionState.status.isGranted) { + { + BottomNavBar( + navController, + listOf( + Screen.Main.navItem, + Screen.Settings.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,34 +204,42 @@ 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") if (!id.isNullOrBlank()) { - //https://dagger.dev/hilt/view-model#assisted-injection + // https://dagger.dev/hilt/view-model#assisted-injection ConfigScreen( navController = navController, id = id, showSnackbarMessage = { message -> showSnackBarMessage(message) }, - focusRequester = focusRequester + focusRequester = focusRequester, ) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Screen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Screen.kt index 686de5d..cd4f5ea 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Screen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Screen.kt @@ -6,28 +6,33 @@ import androidx.compose.material.icons.rounded.QuestionMark import androidx.compose.material.icons.rounded.Settings import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem -sealed class Screen(val route : String) { - data object Main: Screen("main") { - val navItem = BottomNavItem( - name = "Tunnels", - route = route, - icon = Icons.Rounded.Home - ) +sealed class Screen(val route: String) { + data object Main : Screen("main") { + val navItem = + BottomNavItem( + name = "Tunnels", + route = route, + icon = Icons.Rounded.Home, + ) } - data object Settings: Screen("settings") { - val navItem = BottomNavItem( - name = "Settings", - route = route, - icon = Icons.Rounded.Settings - ) - } - data object Support: Screen("support") { - val navItem = BottomNavItem( - name = "Support", - route = route, - icon = Icons.Rounded.QuestionMark - ) - } - data object Config : Screen("config") -} \ No newline at end of file + data object Settings : Screen("settings") { + val navItem = + BottomNavItem( + name = "Settings", + route = route, + icon = Icons.Rounded.Settings, + ) + } + + data object Support : Screen("support") { + val navItem = + BottomNavItem( + name = "Support", + route = route, + icon = Icons.Rounded.QuestionMark, + ) + } + + data object Config : Screen("config") +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt index 3bccc4a..51990dd 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt @@ -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)) @@ -31,11 +31,11 @@ fun ClickableIconButton( imageVector = icon, contentDescription = stringResource(R.string.delete), modifier = - Modifier.size(ButtonDefaults.IconSize).weight(1f, false).clickable { - if (enabled) { - onIconClick() - } - } + Modifier.size(ButtonDefaults.IconSize).weight(1f, false).clickable { + if (enabled) { + onIconClick() + } + }, ) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/PermissionRequestFailedScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/PermissionRequestFailedScreen.kt index 686c73a..11058fb 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/PermissionRequestFailedScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/PermissionRequestFailedScreen.kt @@ -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) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt index 9e059a3..1ed4599 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt @@ -34,30 +34,22 @@ fun RowListItem( ) { Box( modifier = - Modifier - .animateContentSize() - .clip(RoundedCornerShape(30.dp)) - .combinedClickable( - onClick = { - onClick() - }, - onLongClick = { - onHold() - } - ) + Modifier.animateContentSize() + .clip(RoundedCornerShape(30.dp)) + .combinedClickable( + 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() - .padding(end = 10.dp, bottom = 10.dp, start = 10.dp), + 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 diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/SearchBar.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/SearchBar.kt index 4016f31..95ade9e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/SearchBar.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/SearchBar.kt @@ -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,25 +56,24 @@ 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), ) } } }, maxLines = 1, colors = - TextFieldDefaults.colors( - focusedContainerColor = Color.Transparent, - unfocusedContainerColor = Color.Transparent, - disabledContainerColor = Color.Transparent - ), + TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = 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), ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationTextBox.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationTextBox.kt index 2bb3dfa..1226162 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationTextBox.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationTextBox.kt @@ -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 - ), - keyboardActions = keyboardActions + KeyboardOptions( + capitalization = KeyboardCapitalization.None, + imeAction = ImeAction.Done, + ), + keyboardActions = keyboardActions, ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationToggle.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationToggle.kt index a4ae21f..80146c0 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationToggle.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationToggle.kt @@ -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() }, ) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt index 1c3e819..ef9cd20 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt @@ -11,14 +11,11 @@ import androidx.navigation.NavController import androidx.navigation.compose.currentBackStackEntryAsState @Composable -fun BottomNavBar( - navController: NavController, - bottomNavItems: List -) { +fun BottomNavBar(navController: NavController, bottomNavItems: List) { 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", ) - } + }, ) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/AuthorizationPrompt.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/AuthorizationPrompt.kt index 7da6b2e..bf25174 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/AuthorizationPrompt.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/AuthorizationPrompt.kt @@ -11,51 +11,40 @@ 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 { - 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 + 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 } + } if (isBiometricAvailable) { val executor = remember { ContextCompat.getMainExecutor(context) } @@ -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) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/CustomSnackbar.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/CustomSnackbar.kt index cb55319..dd46f9f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/CustomSnackbar.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/CustomSnackbar.kt @@ -37,25 +37,25 @@ fun CustomSnackBar( Snackbar( containerColor = containerColor, modifier = - Modifier.fillMaxWidth( - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f - ).padding(bottom = 100.dp), - shape = RoundedCornerShape(16.dp) + Modifier.fillMaxWidth( + 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)) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/screen/LoadingScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/screen/LoadingScreen.kt index 0e6df0f..ed72273 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/screen/LoadingScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/screen/LoadingScreen.kt @@ -13,10 +13,11 @@ import androidx.compose.ui.unit.dp @Composable fun LoadingScreen() { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top, - modifier = Modifier.fillMaxSize().focusable().padding()) { - Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() } + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + modifier = Modifier.fillMaxSize().focusable().padding(), + ) { + Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/text/SectionTitle.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/text/SectionTitle.kt index 07d543f..2d56e66 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/text/SectionTitle.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/text/SectionTitle.kt @@ -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), ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/InterfaceProxy.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/InterfaceProxy.kt index 8a44324..caf8b7f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/InterfaceProxy.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/InterfaceProxy.kt @@ -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() - } else { - "" - }, - mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "" + listenPort = + if (i.listenPort.isPresent) { + i.listenPort.get().toString().trim() + } else { + "" + }, + mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "", ) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/PeerProxy.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/PeerProxy.kt index 3b57ee7..ad5c3cb 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/PeerProxy.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/PeerProxy.kt @@ -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() - } else { - "" - }, - persistentKeepalive = if (peer.persistentKeepalive.isPresent) { - peer.persistentKeepalive.get() - .toString().trim() - } else { - "" - }, - endpoint = if (peer.endpoint.isPresent) { - peer.endpoint.get().toString() - .trim() - } else { - "" - }, - allowedIps = peer.allowedIps.joinToString(", ").trim() + preSharedKey = + if (peer.preSharedKey.isPresent) { + peer.preSharedKey.get().toBase64().trim() + } else { + "" + }, + persistentKeepalive = + if (peer.persistentKeepalive.isPresent) { + peer.persistentKeepalive.get().toString().trim() + } else { + "" + }, + endpoint = + if (peer.endpoint.isPresent) { + peer.endpoint.get().toString().trim() + } else { + "" + }, + 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") } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt index 45f3bf1..bab109c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt @@ -88,7 +88,8 @@ import kotlinx.coroutines.delay @OptIn( ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class, - ExperimentalFoundationApi::class) + ExperimentalFoundationApi::class, +) @Composable fun ConfigScreen( viewModel: ConfigViewModel = hiltViewModel(), @@ -97,347 +98,374 @@ fun ConfigScreen( showSnackbarMessage: (String) -> Unit, id: String ) { - val context = LocalContext.current - val clipboardManager: ClipboardManager = LocalClipboardManager.current - val keyboardController = LocalSoftwareKeyboardController.current - var showApplicationsDialog by remember { mutableStateOf(false) } - var showAuthPrompt by remember { mutableStateOf(false) } - var isAuthenticated by remember { mutableStateOf(false) } + val context = LocalContext.current + val clipboardManager: ClipboardManager = LocalClipboardManager.current + val keyboardController = LocalSoftwareKeyboardController.current + var showApplicationsDialog by remember { mutableStateOf(false) } + var showAuthPrompt by remember { mutableStateOf(false) } + var isAuthenticated by remember { mutableStateOf(false) } - val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() - LaunchedEffect(Unit) { - viewModel.init(id) - } + LaunchedEffect(Unit) { viewModel.init(id) } - LaunchedEffect(uiState.loading) { - if(!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) { - delay(Constants.FOCUS_REQUEST_DELAY) - focusRequester.requestFocus() - } - } - - if (uiState.loading) { - LoadingScreen() - return - } - - val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }) - - val keyboardOptions = - KeyboardOptions(imeAction = ImeAction.Done) - - val fillMaxHeight = .85f - val fillMaxWidth = .85f - val screenPadding = 5.dp - - val applicationButtonText = { - "Tunneling apps: " + - if (uiState.isAllApplicationsEnabled) { - "all" - } else { - "${uiState.checkedPackageNames.size} " + (if (uiState.include) "included" else "excluded") + LaunchedEffect(uiState.loading) { + if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) { + delay(Constants.FOCUS_REQUEST_DELAY) + focusRequester.requestFocus() } - } + } - if (showAuthPrompt) { - AuthorizationPrompt( - onSuccess = { - showAuthPrompt = false - isAuthenticated = true - }, - onError = { error -> - showAuthPrompt = false - showSnackbarMessage(Event.Error.AuthenticationFailed.message) - }, - onFailure = { - showAuthPrompt = false - showSnackbarMessage(Event.Error.AuthorizationFailed.message) - }) - } + if (uiState.loading) { + LoadingScreen() + return + } - if (showApplicationsDialog) { - val sortedPackages = - remember(uiState.packages) { uiState.packages.sortedBy { viewModel.getPackageLabel(it) } } - AlertDialog(onDismissRequest = { showApplicationsDialog = false }) { - Surface( - tonalElevation = 2.dp, - shadowElevation = 2.dp, - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surface, - modifier = - Modifier.fillMaxWidth() - .fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f)) { - Column(modifier = Modifier.fillMaxWidth()) { - Row( - modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween) { - Text(stringResource(id = R.string.tunnel_all)) - Switch( - checked = uiState.isAllApplicationsEnabled, - onCheckedChange = { viewModel.onAllApplicationsChange(it) }) - } - if (!uiState.isAllApplicationsEnabled) { - Row( - modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween) { - Text(stringResource(id = R.string.include)) - Checkbox( - checked = uiState.include, - onCheckedChange = { viewModel.onIncludeChange(!uiState.include) }) - } - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween) { - Text(stringResource(id = R.string.exclude)) - Checkbox( - checked = !uiState.include, - onCheckedChange = { viewModel.onIncludeChange(!uiState.include) }) - } + val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }) + + val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done) + + val fillMaxHeight = .85f + val fillMaxWidth = .85f + val screenPadding = 5.dp + + val applicationButtonText = { + "Tunneling apps: " + + if (uiState.isAllApplicationsEnabled) { + "all" + } else { + "${uiState.checkedPackageNames.size} " + + (if (uiState.include) "included" else "excluded") + } + } + + if (showAuthPrompt) { + AuthorizationPrompt( + onSuccess = { + showAuthPrompt = false + isAuthenticated = true + }, + onError = { error -> + showAuthPrompt = false + showSnackbarMessage(Event.Error.AuthenticationFailed.message) + }, + onFailure = { + showAuthPrompt = false + showSnackbarMessage(Event.Error.AuthorizationFailed.message) + }, + ) + } + + if (showApplicationsDialog) { + val sortedPackages = + remember(uiState.packages) { + uiState.packages.sortedBy { viewModel.getPackageLabel(it) } + } + AlertDialog(onDismissRequest = { showApplicationsDialog = false }) { + Surface( + tonalElevation = 2.dp, + shadowElevation = 2.dp, + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surface, + modifier = + Modifier.fillMaxWidth() + .fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f), + ) { + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = + Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(stringResource(id = R.string.tunnel_all)) + Switch( + checked = uiState.isAllApplicationsEnabled, + onCheckedChange = { viewModel.onAllApplicationsChange(it) }, + ) } - Row( - modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween) { - SearchBar(viewModel::emitQueriedPackages) - } - Spacer(Modifier.padding(5.dp)) - LazyColumn( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Top, - modifier = Modifier.fillMaxHeight(4 / 5f)) { - items(sortedPackages, key = { it.packageName }) { pack -> + if (!uiState.isAllApplicationsEnabled) { Row( + modifier = + Modifier.fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 7.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxSize().padding(5.dp)) { - Row(modifier = Modifier.fillMaxWidth(fillMaxWidth)) { - val drawable = - pack.applicationInfo?.loadIcon(context.packageManager) - if (drawable != null) { - Image( - painter = DrawablePainter(drawable), - stringResource(id = R.string.icon), - modifier = Modifier.size(50.dp, 50.dp)) - } else { - Icon( - Icons.Rounded.Android, - stringResource(id = R.string.edit), - modifier = Modifier.size(50.dp, 50.dp)) - } - Text( - viewModel.getPackageLabel(pack), - modifier = Modifier.padding(5.dp)) - } - Checkbox( - modifier = Modifier.fillMaxSize(), - checked = - (uiState.checkedPackageNames.contains(pack.packageName)), - onCheckedChange = { - if (it) { - viewModel.onAddCheckedPackage(pack.packageName) - } else { - viewModel.onRemoveCheckedPackage(pack.packageName) - } - }) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(stringResource(id = R.string.include)) + Checkbox( + checked = uiState.include, + onCheckedChange = { + viewModel.onIncludeChange(!uiState.include) + }, + ) } - } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(stringResource(id = R.string.exclude)) + Checkbox( + checked = !uiState.include, + onCheckedChange = { + viewModel.onIncludeChange(!uiState.include) + }, + ) + } + } + Row( + modifier = + Modifier.fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + SearchBar(viewModel::emitQueriedPackages) + } + Spacer(Modifier.padding(5.dp)) + LazyColumn( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier.fillMaxHeight(4 / 5f), + ) { + items(sortedPackages, key = { it.packageName }) { pack -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxSize().padding(5.dp), + ) { + Row(modifier = Modifier.fillMaxWidth(fillMaxWidth)) { + val drawable = + pack.applicationInfo?.loadIcon(context.packageManager) + if (drawable != null) { + Image( + painter = DrawablePainter(drawable), + stringResource(id = R.string.icon), + modifier = Modifier.size(50.dp, 50.dp), + ) + } else { + Icon( + Icons.Rounded.Android, + stringResource(id = R.string.edit), + modifier = Modifier.size(50.dp, 50.dp), + ) + } + Text( + viewModel.getPackageLabel(pack), + modifier = Modifier.padding(5.dp), + ) + } + Checkbox( + modifier = Modifier.fillMaxSize(), + checked = + (uiState.checkedPackageNames.contains( + pack.packageName + )), + onCheckedChange = { + if (it) { + viewModel.onAddCheckedPackage(pack.packageName) + } else { + viewModel.onRemoveCheckedPackage(pack.packageName) + } + }, + ) + } + } + } } - } - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxSize().padding(top = 5.dp), - horizontalArrangement = Arrangement.Center) { - TextButton(onClick = { showApplicationsDialog = false }) { - Text(stringResource(R.string.done)) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxSize().padding(top = 5.dp), + horizontalArrangement = Arrangement.Center, + ) { + TextButton(onClick = { showApplicationsDialog = false }) { + Text(stringResource(R.string.done)) + } } - } + } } - } + } } - } Scaffold( floatingActionButtonPosition = FabPosition.End, floatingActionButton = { - val secondaryColor = MaterialTheme.colorScheme.secondary - val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) - var fobColor by remember { mutableStateOf(secondaryColor) } - FloatingActionButton( - modifier = - Modifier.padding(bottom = 90.dp).onFocusChanged { - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - fobColor = if (it.isFocused) hoverColor else secondaryColor + val secondaryColor = MaterialTheme.colorScheme.secondary + val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) + var fobColor by remember { mutableStateOf(secondaryColor) } + FloatingActionButton( + modifier = + Modifier.padding(bottom = 90.dp).onFocusChanged { + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + fobColor = if (it.isFocused) hoverColor else secondaryColor + } + }, + onClick = { + viewModel.onSaveAllChanges().let { + when (it) { + is Result.Success -> { + showSnackbarMessage(it.data.message) + navController.navigate(Screen.Main.route) + } + is Result.Error -> showSnackbarMessage(it.error.message) + } } - }, - onClick = { - viewModel.onSaveAllChanges().let { - when (it) { - is Result.Success -> { - showSnackbarMessage(it.data.message) - navController.navigate(Screen.Main.route) - } - is Result.Error -> showSnackbarMessage(it.error.message) - } - } - }, - containerColor = fobColor, - shape = RoundedCornerShape(16.dp)) { + }, + containerColor = fobColor, + shape = RoundedCornerShape(16.dp), + ) { Icon( imageVector = Icons.Rounded.Save, contentDescription = stringResource(id = R.string.save_changes), - tint = Color.DarkGray) - } - }) { - Column { + tint = Color.DarkGray, + ) + } + }, + ) { + Column { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top, modifier = - Modifier.verticalScroll(rememberScrollState()).weight(1f, true).fillMaxSize()) { - Surface( - tonalElevation = 2.dp, - shadowElevation = 2.dp, - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surface, - modifier = - (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + Modifier.verticalScroll(rememberScrollState()).weight(1f, true).fillMaxSize(), + ) { + Surface( + tonalElevation = 2.dp, + shadowElevation = 2.dp, + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surface, + modifier = + (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { Modifier.fillMaxHeight(fillMaxHeight).fillMaxWidth(fillMaxWidth) - } else { + } else { Modifier.fillMaxWidth(fillMaxWidth) - }) - .padding(top = 50.dp, bottom = 10.dp)) { - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Top, - modifier = Modifier.padding(15.dp).focusGroup()) { - SectionTitle( - 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)) - OutlinedTextField( - modifier = - Modifier.fillMaxWidth().clickable { - showAuthPrompt = true - }, - value = uiState.interfaceProxy.privateKey, - visualTransformation = - if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || - isAuthenticated) - VisualTransformation.None - else PasswordVisualTransformation(), - enabled = - (id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated, - onValueChange = { value -> viewModel.onPrivateKeyChange(value) }, - trailingIcon = { - IconButton( - modifier = Modifier.focusRequester(FocusRequester.Default), - onClick = { viewModel.generateKeyPair() }) { - Icon( - Icons.Rounded.Refresh, - stringResource(R.string.rotate_keys), - tint = Color.White) - } - }, - label = { Text(stringResource(R.string.private_key)) }, - singleLine = true, - placeholder = { Text(stringResource(R.string.base64_key)) }, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions) - OutlinedTextField( - modifier = - Modifier - .fillMaxWidth() - .focusRequester(FocusRequester.Default), - value = uiState.interfaceProxy.publicKey, - enabled = false, - onValueChange = {}, - trailingIcon = { - IconButton( - modifier = Modifier.focusRequester(FocusRequester.Default), - onClick = { - clipboardManager.setText( - AnnotatedString(uiState.interfaceProxy.publicKey)) - }) { - Icon( - Icons.Rounded.ContentCopy, - stringResource(R.string.copy_public_key), - tint = Color.White) - } - }, - label = { Text(stringResource(R.string.public_key)) }, - singleLine = true, - placeholder = { Text(stringResource(R.string.base64_key)) }, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions) - Row(modifier = Modifier.fillMaxWidth()) { - ConfigurationTextBox( - value = uiState.interfaceProxy.addresses, - onValueChange = { value -> - viewModel.onAddressesChanged(value) + }) + .padding(top = 50.dp, bottom = 10.dp), + ) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier.padding(15.dp).focusGroup(), + ) { + SectionTitle( + 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), + ) + OutlinedTextField( + modifier = Modifier.fillMaxWidth().clickable { showAuthPrompt = true }, + value = uiState.interfaceProxy.privateKey, + visualTransformation = + if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated) + VisualTransformation.None + else PasswordVisualTransformation(), + enabled = (id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated, + onValueChange = { value -> viewModel.onPrivateKeyChange(value) }, + trailingIcon = { + IconButton( + modifier = Modifier.focusRequester(FocusRequester.Default), + onClick = { viewModel.generateKeyPair() }, + ) { + Icon( + Icons.Rounded.Refresh, + stringResource(R.string.rotate_keys), + tint = Color.White, + ) + } + }, + label = { Text(stringResource(R.string.private_key)) }, + singleLine = true, + placeholder = { Text(stringResource(R.string.base64_key)) }, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + ) + OutlinedTextField( + modifier = + Modifier.fillMaxWidth().focusRequester(FocusRequester.Default), + value = uiState.interfaceProxy.publicKey, + enabled = false, + onValueChange = {}, + trailingIcon = { + IconButton( + modifier = Modifier.focusRequester(FocusRequester.Default), + onClick = { + clipboardManager.setText( + AnnotatedString(uiState.interfaceProxy.publicKey), + ) }, - keyboardActions = keyboardActions, - label = stringResource(R.string.addresses), - hint = stringResource(R.string.comma_separated_list), - modifier = - Modifier - .fillMaxWidth(3 / 5f) - .padding(end = 5.dp)) - ConfigurationTextBox( - value = uiState.interfaceProxy.listenPort, - onValueChange = { value -> - viewModel.onListenPortChanged(value) - }, - keyboardActions = keyboardActions, - label = stringResource(R.string.listen_port), - hint = stringResource(R.string.random), - modifier = Modifier.width(IntrinsicSize.Min)) - } - Row(modifier = Modifier.fillMaxWidth()) { - ConfigurationTextBox( - value = uiState.interfaceProxy.dnsServers, - 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)) - 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)) - } - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxSize().padding(top = 5.dp), - horizontalArrangement = Arrangement.Center) { - TextButton(onClick = { showApplicationsDialog = true }) { - Text(applicationButtonText()) - } - } + ) { + Icon( + Icons.Rounded.ContentCopy, + stringResource(R.string.copy_public_key), + tint = Color.White, + ) + } + }, + label = { Text(stringResource(R.string.public_key)) }, + singleLine = true, + placeholder = { Text(stringResource(R.string.base64_key)) }, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + ) + Row(modifier = Modifier.fillMaxWidth()) { + ConfigurationTextBox( + value = uiState.interfaceProxy.addresses, + onValueChange = { value -> viewModel.onAddressesChanged(value) }, + keyboardActions = keyboardActions, + label = stringResource(R.string.addresses), + hint = stringResource(R.string.comma_separated_list), + modifier = Modifier.fillMaxWidth(3 / 5f).padding(end = 5.dp), + ) + ConfigurationTextBox( + value = uiState.interfaceProxy.listenPort, + onValueChange = { value -> viewModel.onListenPortChanged(value) }, + keyboardActions = keyboardActions, + label = stringResource(R.string.listen_port), + hint = stringResource(R.string.random), + modifier = Modifier.width(IntrinsicSize.Min), + ) + } + Row(modifier = Modifier.fillMaxWidth()) { + ConfigurationTextBox( + value = uiState.interfaceProxy.dnsServers, + 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), + ) + 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), + ) + } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxSize().padding(top = 5.dp), + horizontalArrangement = Arrangement.Center, + ) { + TextButton(onClick = { showApplicationsDialog = true }) { + Text(applicationButtonText()) } - } - uiState.proxyPeers.forEachIndexed { index, peer -> + } + } + } + uiState.proxyPeers.forEachIndexed { index, peer -> Surface( tonalElevation = 2.dp, shadowElevation = 2.dp, @@ -445,106 +473,118 @@ fun ConfigScreen( color = MaterialTheme.colorScheme.surface, modifier = (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - Modifier.fillMaxHeight(fillMaxHeight).fillMaxWidth(fillMaxWidth) + Modifier.fillMaxHeight(fillMaxHeight).fillMaxWidth(fillMaxWidth) } else { - Modifier.fillMaxWidth(fillMaxWidth) + Modifier.fillMaxWidth(fillMaxWidth) }) - .padding(top = 10.dp, bottom = 10.dp)) { - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Top, - modifier = - Modifier.padding(horizontal = 15.dp).padding(bottom = 10.dp)) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth().padding(horizontal = 5.dp)) { - SectionTitle( - stringResource(R.string.peer), padding = screenPadding) - IconButton(onClick = { viewModel.onDeletePeer(index) }) { - Icon(Icons.Rounded.Delete, stringResource(R.string.delete)) - } - } - - ConfigurationTextBox( - value = peer.publicKey, - onValueChange = { value -> - viewModel.onPeerPublicKeyChange(index, value) - }, - keyboardActions = keyboardActions, - label = stringResource(R.string.public_key), - hint = stringResource(R.string.base64_key), - modifier = Modifier.fillMaxWidth()) - ConfigurationTextBox( - value = peer.preSharedKey, - onValueChange = { value -> - viewModel.onPreSharedKeyChange(index, value) - }, - keyboardActions = keyboardActions, - label = stringResource(R.string.preshared_key), - hint = stringResource(R.string.optional), - modifier = Modifier.fillMaxWidth()) - OutlinedTextField( - modifier = Modifier.fillMaxWidth(), - value = peer.persistentKeepalive, - enabled = true, - onValueChange = { value -> - viewModel.onPersistentKeepaliveChanged(index, value) - }, - trailingIcon = { - Text( - stringResource(R.string.seconds), - modifier = Modifier.padding(end = 10.dp)) - }, - label = { Text(stringResource(R.string.persistent_keepalive)) }, - singleLine = true, - placeholder = { - Text(stringResource(R.string.optional_no_recommend)) - }, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions) - ConfigurationTextBox( - value = peer.endpoint, - onValueChange = { value -> - viewModel.onEndpointChange(index, value) - }, - keyboardActions = keyboardActions, - label = stringResource(R.string.endpoint), - hint = stringResource(R.string.endpoint).lowercase(), - modifier = Modifier.fillMaxWidth()) - OutlinedTextField( - modifier = Modifier.fillMaxWidth(), - value = peer.allowedIps, - enabled = true, - onValueChange = { value -> - viewModel.onAllowedIpsChange(index, value) - }, - label = { Text(stringResource(R.string.allowed_ips)) }, - singleLine = true, - placeholder = { - Text(stringResource(R.string.comma_separated_list)) - }, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions) - } - } - } - Row( - horizontalArrangement = Arrangement.SpaceEvenly, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxSize().padding(bottom = 140.dp)) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center) { - TextButton(onClick = { viewModel.addEmptyPeer() }) { - Text(stringResource(R.string.add_peer)) - } + .padding(top = 10.dp, bottom = 10.dp), + ) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier.padding(horizontal = 15.dp).padding(bottom = 10.dp), + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().padding(horizontal = 5.dp), + ) { + SectionTitle( + stringResource(R.string.peer), + padding = screenPadding, + ) + IconButton(onClick = { viewModel.onDeletePeer(index) }) { + Icon(Icons.Rounded.Delete, stringResource(R.string.delete)) + } } - } + + ConfigurationTextBox( + value = peer.publicKey, + onValueChange = { value -> + viewModel.onPeerPublicKeyChange(index, value) + }, + keyboardActions = keyboardActions, + label = stringResource(R.string.public_key), + hint = stringResource(R.string.base64_key), + modifier = Modifier.fillMaxWidth(), + ) + ConfigurationTextBox( + value = peer.preSharedKey, + onValueChange = { value -> + viewModel.onPreSharedKeyChange(index, value) + }, + keyboardActions = keyboardActions, + label = stringResource(R.string.preshared_key), + hint = stringResource(R.string.optional), + modifier = Modifier.fillMaxWidth(), + ) + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = peer.persistentKeepalive, + enabled = true, + onValueChange = { value -> + viewModel.onPersistentKeepaliveChanged(index, value) + }, + trailingIcon = { + Text( + stringResource(R.string.seconds), + modifier = Modifier.padding(end = 10.dp), + ) + }, + label = { Text(stringResource(R.string.persistent_keepalive)) }, + singleLine = true, + placeholder = { + Text(stringResource(R.string.optional_no_recommend)) + }, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + ) + ConfigurationTextBox( + value = peer.endpoint, + onValueChange = { value -> + viewModel.onEndpointChange(index, value) + }, + keyboardActions = keyboardActions, + label = stringResource(R.string.endpoint), + hint = stringResource(R.string.endpoint).lowercase(), + modifier = Modifier.fillMaxWidth(), + ) + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = peer.allowedIps, + enabled = true, + onValueChange = { value -> + viewModel.onAllowedIpsChange(index, value) + }, + label = { Text(stringResource(R.string.allowed_ips)) }, + singleLine = true, + placeholder = { + Text(stringResource(R.string.comma_separated_list)) + }, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + ) + } + } + } + Row( + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxSize().padding(bottom = 140.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + TextButton(onClick = { viewModel.addEmptyPeer() }) { + Text(stringResource(R.string.add_peer)) + } + } } - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - Spacer(modifier = Modifier.weight(.17f)) } - } + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + Spacer(modifier = Modifier.weight(.17f)) + } } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigUiState.kt index dbeba0a..7abc66c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigUiState.kt @@ -11,8 +11,8 @@ data class ConfigUiState( val packages: Packages = emptyList(), val checkedPackageNames: List = emptyList(), val include: Boolean = true, - val isAllApplicationsEnabled : Boolean = false, + val isAllApplicationsEnabled: Boolean = false, val loading: Boolean = true, val tunnel: TunnelConfig? = null, val tunnelName: String = "" -) \ No newline at end of file +) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt index 82a2990..f6254e4 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt @@ -41,272 +41,312 @@ constructor( private val settingsRepository: SettingsRepository, ) : ViewModel() { - private val packageManager = application.packageManager + private val packageManager = application.packageManager - private val _uiState = MutableStateFlow(ConfigUiState()) - val uiState = _uiState.asStateFlow() + private val _uiState = MutableStateFlow(ConfigUiState()) + val uiState = _uiState.asStateFlow() - fun init(tunnelId : String) = viewModelScope.launch(Dispatchers.IO) { - val packages = getQueriedPackages("") - val state = if(tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) { - val tunnelConfig = - tunnelConfigRepository.getAll().firstOrNull { it.id.toString() == tunnelId } - if (tunnelConfig != null) { - val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick) - val proxyPeers = config.peers.map { PeerProxy.from(it) } - val proxyInterface = InterfaceProxy.from(config.`interface`) - var include = true - var isAllApplicationsEnabled = false - val checkedPackages = - if (config.`interface`.includedApplications.isNotEmpty()) { - config.`interface`.includedApplications - } else if (config.`interface`.excludedApplications.isNotEmpty()) { - include = false - config.`interface`.excludedApplications + fun init(tunnelId: String) = + viewModelScope.launch(Dispatchers.IO) { + val packages = getQueriedPackages("") + val state = + if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) { + val tunnelConfig = + tunnelConfigRepository.getAll().firstOrNull { it.id.toString() == tunnelId } + if (tunnelConfig != null) { + val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick) + val proxyPeers = config.peers.map { PeerProxy.from(it) } + val proxyInterface = InterfaceProxy.from(config.`interface`) + var include = true + var isAllApplicationsEnabled = false + val checkedPackages = + if (config.`interface`.includedApplications.isNotEmpty()) { + config.`interface`.includedApplications + } else if (config.`interface`.excludedApplications.isNotEmpty()) { + include = false + config.`interface`.excludedApplications + } else { + isAllApplicationsEnabled = true + emptySet() + } + ConfigUiState( + proxyPeers, + proxyInterface, + packages, + checkedPackages.toList(), + include, + isAllApplicationsEnabled, + false, + tunnelConfig, + tunnelConfig.name, + ) } else { - isAllApplicationsEnabled = true - emptySet() + ConfigUiState(loading = false, packages = packages) } - ConfigUiState( - proxyPeers, - proxyInterface, - packages, - checkedPackages.toList(), - include, - isAllApplicationsEnabled, - false, - tunnelConfig, - tunnelConfig.name) - } else { - ConfigUiState(loading = false, packages = packages) - } + } else { + ConfigUiState(loading = false, packages = packages) + } + _uiState.value = state + } + + fun onTunnelNameChange(name: String) { + _uiState.value = _uiState.value.copy(tunnelName = name) + } + + fun onIncludeChange(include: Boolean) { + _uiState.value = _uiState.value.copy(include = include) + } + + fun onAddCheckedPackage(packageName: String) { + _uiState.value = + _uiState.value.copy( + checkedPackageNames = _uiState.value.checkedPackageNames + packageName + ) + } + + fun onAllApplicationsChange(isAllApplicationsEnabled: Boolean) { + _uiState.value = _uiState.value.copy(isAllApplicationsEnabled = isAllApplicationsEnabled) + } + + fun onRemoveCheckedPackage(packageName: String) { + _uiState.value = + _uiState.value.copy( + checkedPackageNames = _uiState.value.checkedPackageNames - packageName + ) + } + + private fun getQueriedPackages(query: String): List { + return getAllInternetCapablePackages().filter { + getPackageLabel(it).lowercase().contains(query.lowercase()) + } + } + + fun getPackageLabel(packageInfo: PackageInfo): String { + return packageInfo.applicationInfo.loadLabel(application.packageManager).toString() + } + + private fun getAllInternetCapablePackages(): List { + return getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET)) + } + + private fun getPackagesHoldingPermissions(permissions: Array): List { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + packageManager.getPackagesHoldingPermissions( + permissions, + PackageManager.PackageInfoFlags.of(0L), + ) } else { - ConfigUiState(loading = false, packages = packages) + packageManager.getPackagesHoldingPermissions(permissions, 0) } - _uiState.value = state } - fun onTunnelNameChange(name: String) { - _uiState.value = _uiState.value.copy(tunnelName = name) - } - fun onIncludeChange(include: Boolean) { - _uiState.value = _uiState.value.copy(include = include) - } - - fun onAddCheckedPackage(packageName: String) { - _uiState.value = - _uiState.value.copy(checkedPackageNames = _uiState.value.checkedPackageNames + packageName) - } - - fun onAllApplicationsChange(isAllApplicationsEnabled: Boolean) { - _uiState.value = _uiState.value.copy(isAllApplicationsEnabled = isAllApplicationsEnabled) - } - - fun onRemoveCheckedPackage(packageName: String) { - _uiState.value = - _uiState.value.copy(checkedPackageNames = _uiState.value.checkedPackageNames - packageName) - } - - private fun getQueriedPackages(query: String): List { - return getAllInternetCapablePackages().filter { - getPackageLabel(it).lowercase().contains(query.lowercase()) + private fun isAllApplicationsEnabled(): Boolean { + return _uiState.value.isAllApplicationsEnabled } - } - fun getPackageLabel(packageInfo: PackageInfo): String { - return packageInfo.applicationInfo.loadLabel(application.packageManager).toString() - } + private fun saveConfig(tunnelConfig: TunnelConfig) = + viewModelScope.launch { tunnelConfigRepository.save(tunnelConfig) } - private fun getAllInternetCapablePackages(): List { - return getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET)) - } - - private fun getPackagesHoldingPermissions(permissions: Array): List { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - packageManager.getPackagesHoldingPermissions( - permissions, PackageManager.PackageInfoFlags.of(0L)) - } else { - packageManager.getPackagesHoldingPermissions(permissions, 0) - } - } - - private fun isAllApplicationsEnabled(): Boolean { - return _uiState.value.isAllApplicationsEnabled - } - - private fun saveConfig(tunnelConfig: TunnelConfig) = - viewModelScope.launch { - tunnelConfigRepository.save(tunnelConfig) - } - - private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) = - viewModelScope.launch { - if (tunnelConfig != null) { - saveConfig(tunnelConfig).join() - WireGuardAutoTunnel.requestTileServiceStateUpdate() - updateSettingsDefaultTunnel(tunnelConfig) + private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) = + viewModelScope.launch { + if (tunnelConfig != null) { + saveConfig(tunnelConfig).join() + WireGuardAutoTunnel.requestTileServiceStateUpdate() + updateSettingsDefaultTunnel(tunnelConfig) + } } - } - private suspend fun updateSettingsDefaultTunnel(tunnelConfig: TunnelConfig) { - val settings = settingsRepository.getSettingsFlow().first() - if (settings.defaultTunnel != null) { - if (tunnelConfig.id == TunnelConfig.from(settings.defaultTunnel!!).id) { - settingsRepository.save(settings.copy(defaultTunnel = tunnelConfig.toString())) - } - } - } - - private fun buildPeerListFromProxyPeers(): List { - return _uiState.value.proxyPeers.map { - val builder = Peer.Builder() - if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim()) - if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim()) - if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.trim()) - if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.trim()) - if (it.persistentKeepalive.isNotEmpty()) { - builder.parsePersistentKeepalive(it.persistentKeepalive.trim()) - } - builder.build() - } - } - - private fun emptyCheckedPackagesList() { - _uiState.value = _uiState.value.copy(checkedPackageNames = emptyList()) - } - - private fun buildInterfaceListFromProxyInterface(): Interface { - val builder = Interface.Builder() - builder.parsePrivateKey(_uiState.value.interfaceProxy.privateKey.trim()) - builder.parseAddresses(_uiState.value.interfaceProxy.addresses.trim()) - builder.parseDnsServers(_uiState.value.interfaceProxy.dnsServers.trim()) - if (_uiState.value.interfaceProxy.mtu.isNotEmpty()) - builder.parseMtu(_uiState.value.interfaceProxy.mtu.trim()) - if (_uiState.value.interfaceProxy.listenPort.isNotEmpty()) { - builder.parseListenPort(_uiState.value.interfaceProxy.listenPort.trim()) - } - if (isAllApplicationsEnabled()) emptyCheckedPackagesList() - if (_uiState.value.include) builder.includeApplications(_uiState.value.checkedPackageNames) - if (!_uiState.value.include) builder.excludeApplications(_uiState.value.checkedPackageNames) - return builder.build() - } - - fun onSaveAllChanges(): Result { - return try { - val peerList = buildPeerListFromProxyPeers() - val wgInterface = buildInterfaceListFromProxyInterface() - val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build() - val tunnelConfig = - _uiState.value.tunnel?.copy( - name = _uiState.value.tunnelName, wgQuick = config.toWgQuickString()) - updateTunnelConfig(tunnelConfig) - Result.Success(Event.Message.ConfigSaved) - } catch (e: Exception) { - Result.Error(Event.Error.Exception(e)) - } - } - - fun onPeerPublicKeyChange(index: Int, value: String) { - _uiState.value = - _uiState.value.copy( - proxyPeers = - _uiState.value.proxyPeers.update( - index, _uiState.value.proxyPeers[index].copy(publicKey = value))) - } - - fun onPreSharedKeyChange(index: Int, value: String) { - _uiState.value = - _uiState.value.copy( - proxyPeers = - _uiState.value.proxyPeers.update( - index, _uiState.value.proxyPeers[index].copy(preSharedKey = value))) - } - - fun onEndpointChange(index: Int, value: String) { - _uiState.value = - _uiState.value.copy( - proxyPeers = - _uiState.value.proxyPeers.update( - index, _uiState.value.proxyPeers[index].copy(endpoint = value))) - } - - fun onAllowedIpsChange(index: Int, value: String) { - _uiState.value = - _uiState.value.copy( - proxyPeers = - _uiState.value.proxyPeers.update( - index, _uiState.value.proxyPeers[index].copy(allowedIps = value))) - } - - fun onPersistentKeepaliveChanged(index: Int, value: String) { - _uiState.value = - _uiState.value.copy( - proxyPeers = - _uiState.value.proxyPeers.update( - index, _uiState.value.proxyPeers[index].copy(persistentKeepalive = value))) - } - - fun onDeletePeer(index: Int) { - _uiState.value = _uiState.value.copy( - proxyPeers = _uiState.value.proxyPeers.removeAt(index) - ) - } - - fun addEmptyPeer() { - _uiState.value = _uiState.value.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy()) - } - - fun generateKeyPair() { - val keyPair = KeyPair() - _uiState.value = - _uiState.value.copy( - interfaceProxy = - _uiState.value.interfaceProxy.copy( - privateKey = keyPair.privateKey.toBase64(), - publicKey = keyPair.publicKey.toBase64())) - } - - fun onAddressesChanged(value: String) { - _uiState.value = - _uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value)) - } - - fun onListenPortChanged(value: String) { - _uiState.value = - _uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value)) - } - - fun onDnsServersChanged(value: String) { - _uiState.value = - _uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value)) - } - - fun onMtuChanged(value: String) { - _uiState.value = - _uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value)) - } - - private fun onInterfacePublicKeyChange(value: String) { - _uiState.value = - _uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value)) - } - - fun onPrivateKeyChange(value: String) { - _uiState.value = - _uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value)) - if (NumberUtils.isValidKey(value)) { - val pair = KeyPair(Key.fromBase64(value)) - onInterfacePublicKeyChange(pair.publicKey.toBase64()) - } else { - onInterfacePublicKeyChange("") - } - } - - fun emitQueriedPackages(query: String) { - val packages = - getAllInternetCapablePackages().filter { - getPackageLabel(it).lowercase().contains(query.lowercase()) + private suspend fun updateSettingsDefaultTunnel(tunnelConfig: TunnelConfig) { + val settings = settingsRepository.getSettingsFlow().first() + if (settings.defaultTunnel != null) { + if (tunnelConfig.id == TunnelConfig.from(settings.defaultTunnel!!).id) { + settingsRepository.save(settings.copy(defaultTunnel = tunnelConfig.toString())) + } } - _uiState.value = _uiState.value.copy(packages = packages) - } + } + + private fun buildPeerListFromProxyPeers(): List { + return _uiState.value.proxyPeers.map { + val builder = Peer.Builder() + if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim()) + if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim()) + if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.trim()) + if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.trim()) + if (it.persistentKeepalive.isNotEmpty()) { + builder.parsePersistentKeepalive(it.persistentKeepalive.trim()) + } + builder.build() + } + } + + private fun emptyCheckedPackagesList() { + _uiState.value = _uiState.value.copy(checkedPackageNames = emptyList()) + } + + private fun buildInterfaceListFromProxyInterface(): Interface { + val builder = Interface.Builder() + builder.parsePrivateKey(_uiState.value.interfaceProxy.privateKey.trim()) + builder.parseAddresses(_uiState.value.interfaceProxy.addresses.trim()) + if (_uiState.value.interfaceProxy.dnsServers.isNotEmpty()) { + builder.parseDnsServers(_uiState.value.interfaceProxy.dnsServers.trim()) + } + if (_uiState.value.interfaceProxy.mtu.isNotEmpty()) + builder.parseMtu(_uiState.value.interfaceProxy.mtu.trim()) + if (_uiState.value.interfaceProxy.listenPort.isNotEmpty()) { + builder.parseListenPort(_uiState.value.interfaceProxy.listenPort.trim()) + } + if (isAllApplicationsEnabled()) emptyCheckedPackagesList() + if (_uiState.value.include) builder.includeApplications(_uiState.value.checkedPackageNames) + if (!_uiState.value.include) builder.excludeApplications(_uiState.value.checkedPackageNames) + return builder.build() + } + + fun onSaveAllChanges(): Result { + return try { + val peerList = buildPeerListFromProxyPeers() + val wgInterface = buildInterfaceListFromProxyInterface() + val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build() + val tunnelConfig = + _uiState.value.tunnel?.copy( + name = _uiState.value.tunnelName, + wgQuick = config.toWgQuickString(), + ) + updateTunnelConfig(tunnelConfig) + Result.Success(Event.Message.ConfigSaved) + } catch (e: Exception) { + Result.Error(Event.Error.Exception(e)) + } + } + + fun onPeerPublicKeyChange(index: Int, value: String) { + _uiState.value = + _uiState.value.copy( + proxyPeers = + _uiState.value.proxyPeers.update( + index, + _uiState.value.proxyPeers[index].copy(publicKey = value), + ), + ) + } + + fun onPreSharedKeyChange(index: Int, value: String) { + _uiState.value = + _uiState.value.copy( + proxyPeers = + _uiState.value.proxyPeers.update( + index, + _uiState.value.proxyPeers[index].copy(preSharedKey = value), + ), + ) + } + + fun onEndpointChange(index: Int, value: String) { + _uiState.value = + _uiState.value.copy( + proxyPeers = + _uiState.value.proxyPeers.update( + index, + _uiState.value.proxyPeers[index].copy(endpoint = value), + ), + ) + } + + fun onAllowedIpsChange(index: Int, value: String) { + _uiState.value = + _uiState.value.copy( + proxyPeers = + _uiState.value.proxyPeers.update( + index, + _uiState.value.proxyPeers[index].copy(allowedIps = value), + ), + ) + } + + fun onPersistentKeepaliveChanged(index: Int, value: String) { + _uiState.value = + _uiState.value.copy( + proxyPeers = + _uiState.value.proxyPeers.update( + index, + _uiState.value.proxyPeers[index].copy(persistentKeepalive = value), + ), + ) + } + + fun onDeletePeer(index: Int) { + _uiState.value = + _uiState.value.copy( + proxyPeers = _uiState.value.proxyPeers.removeAt(index), + ) + } + + fun addEmptyPeer() { + _uiState.value = _uiState.value.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy()) + } + + fun generateKeyPair() { + val keyPair = KeyPair() + _uiState.value = + _uiState.value.copy( + interfaceProxy = + _uiState.value.interfaceProxy.copy( + privateKey = keyPair.privateKey.toBase64(), + publicKey = keyPair.publicKey.toBase64(), + ), + ) + } + + fun onAddressesChanged(value: String) { + _uiState.value = + _uiState.value.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value) + ) + } + + fun onListenPortChanged(value: String) { + _uiState.value = + _uiState.value.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value) + ) + } + + fun onDnsServersChanged(value: String) { + _uiState.value = + _uiState.value.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value) + ) + } + + fun onMtuChanged(value: String) { + _uiState.value = + _uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value)) + } + + private fun onInterfacePublicKeyChange(value: String) { + _uiState.value = + _uiState.value.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value) + ) + } + + fun onPrivateKeyChange(value: String) { + _uiState.value = + _uiState.value.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value) + ) + if (NumberUtils.isValidKey(value)) { + val pair = KeyPair(Key.fromBase64(value)) + onInterfacePublicKeyChange(pair.publicKey.toBase64()) + } else { + onInterfacePublicKeyChange("") + } + } + + fun emitQueriedPackages(query: String) { + val packages = + getAllInternetCapablePackages().filter { + getPackageLabel(it).lowercase().contains(query.lowercase()) + } + _uiState.value = _uiState.value.copy(packages = packages) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt index dfc2ab7..b45f2d2 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt @@ -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) @@ -118,370 +119,472 @@ fun MainScreen( showSnackbarMessage: (String) -> Unit, navController: NavController ) { - val haptic = LocalHapticFeedback.current - val context = LocalContext.current - val isVisible = rememberSaveable { mutableStateOf(true) } - val scope = rememberCoroutineScope { Dispatchers.IO } + val haptic = LocalHapticFeedback.current + val context = LocalContext.current + val isVisible = rememberSaveable { mutableStateOf(true) } + val scope = rememberCoroutineScope { Dispatchers.IO } - val sheetState = rememberModalBottomSheetState() - var showBottomSheet by remember { mutableStateOf(false) } + val sheetState = rememberModalBottomSheetState() + var showBottomSheet by remember { mutableStateOf(false) } - var showPrimaryChangeAlertDialog by remember { mutableStateOf(false) } - var selectedTunnel by remember { mutableStateOf(null) } - val uiState by viewModel.uiState.collectAsStateWithLifecycle() + var showPrimaryChangeAlertDialog by remember { mutableStateOf(false) } + var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) } + var selectedTunnel by remember { mutableStateOf(null) } + val uiState by viewModel.uiState.collectAsStateWithLifecycle() - LaunchedEffect(uiState.loading) { - if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) { - delay(Constants.FOCUS_REQUEST_DELAY) - focusRequester.requestFocus() + LaunchedEffect(uiState.loading) { + if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) { + delay(Constants.FOCUS_REQUEST_DELAY) + focusRequester.requestFocus() + } } - } - if (uiState.loading) { - LoadingScreen() - return - } + if (uiState.loading) { + LoadingScreen() + return + } - val tunnelFileImportResultLauncher = - rememberLauncherForActivityResult( - object : ActivityResultContracts.GetContent() { - override fun createIntent(context: Context, input: String): Intent { - val intent = super.createIntent(context, input) + val tunnelFileImportResultLauncher = + rememberLauncherForActivityResult( + object : ActivityResultContracts.GetContent() { + override fun createIntent(context: Context, input: String): Intent { + val intent = super.createIntent(context, input) - /* AndroidTV now comes with stubs that do nothing but display a Toast less helpful than - * what we can do, so detect this and throw an exception that we can catch later. */ - val activitiesToResolveIntent = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - context.packageManager.queryIntentActivities( - intent, - PackageManager.ResolveInfoFlags.of( - PackageManager.MATCH_DEFAULT_ONLY.toLong())) - } else { - context.packageManager.queryIntentActivities( - intent, PackageManager.MATCH_DEFAULT_ONLY) - } - 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 -> + /* AndroidTV now comes with stubs that do nothing but display a Toast less helpful than + * what we can do, so detect this and throw an exception that we can catch later. */ + val activitiesToResolveIntent = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.packageManager.queryIntentActivities( + intent, + PackageManager.ResolveInfoFlags.of( + PackageManager.MATCH_DEFAULT_ONLY.toLong(), + ), + ) + } else { + context.packageManager.queryIntentActivities( + intent, + PackageManager.MATCH_DEFAULT_ONLY, + ) + } + 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 -> if (data == null) return@rememberLauncherForActivityResult scope.launch { - viewModel.onTunnelFileSelected(data).let { - when (it) { - is Result.Error -> showSnackbarMessage(it.error.message) - is Result.Success -> {} + viewModel.onTunnelFileSelected(data).let { + when (it) { + is Result.Error -> showSnackbarMessage(it.error.message) + is Result.Success -> {} + } } - } } - } - val scanLauncher = - rememberLauncherForActivityResult( - contract = ScanContract(), - onResult = { - if (it.contents != null) { - scope.launch { - viewModel.onTunnelQrResult(it.contents).let { result -> - when (result) { - is Result.Success -> {} - is Result.Error -> showSnackbarMessage(result.error.message) - } - } - } - } - }) - - AnimatedVisibility(showPrimaryChangeAlertDialog) { - AlertDialog( - onDismissRequest = { showPrimaryChangeAlertDialog = false }, - confirmButton = { - TextButton( - onClick = { - viewModel.onDefaultTunnelChange(selectedTunnel) - showPrimaryChangeAlertDialog = false - selectedTunnel = null - }) { - Text(text = stringResource(R.string.okay)) - } - }, - dismissButton = { - TextButton(onClick = { showPrimaryChangeAlertDialog = false }) { - Text(text = stringResource(R.string.cancel)) - } - }, - title = { Text(text = stringResource(R.string.primary_tunnel_change)) }, - text = { Text(text = stringResource(R.string.primary_tunnel_change_question)) }) - } - - fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) { - if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop() - } - - Scaffold( - modifier = - Modifier.pointerInput(Unit) { - detectTapGestures( - onTap = { - if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) selectedTunnel = null - }) - }, - floatingActionButtonPosition = FabPosition.End, - topBar = { - if (uiState.settings.isAutoTunnelEnabled) - TopAppBar( - title = { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.requiredWidth(LocalConfiguration.current.screenWidthDp.dp).padding(end = 5.dp)) { - Row { - Icon( - Icons.Rounded.Bolt, - stringResource(id = R.string.auto), - modifier = Modifier.size(25.dp), - tint = if(uiState.settings.isAutoTunnelPaused) Color.Gray else mint) - Text( - "Auto-tunneling: ${if(uiState.settings.isAutoTunnelPaused) "paused" else "active" }", - style = typography.bodyLarge, - modifier = Modifier.padding(start = 10.dp)) - } - if(uiState.settings.isAutoTunnelPaused) TextButton( - onClick = { viewModel.resumeAutoTunneling() }, - modifier = Modifier.padding(end = 10.dp)) { - Text("Resume") - } else TextButton( - onClick = { viewModel.pauseAutoTunneling() }, - modifier = Modifier.padding(end = 10.dp)) { - Text("Pause") - } - } - }, - ) - }, - floatingActionButton = { - AnimatedVisibility( - visible = isVisible.value, - enter = slideInVertically(initialOffsetY = { it * 2 }), - exit = slideOutVertically(targetOffsetY = { it * 2 })) { - val secondaryColor = MaterialTheme.colorScheme.secondary - val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) - var fobColor by remember { mutableStateOf(secondaryColor) } - FloatingActionButton( - modifier = - (if (WireGuardAutoTunnel.isRunningOnAndroidTv() && - uiState.tunnels.isEmpty()) - Modifier.focusRequester(focusRequester) - else Modifier) - .padding(bottom = 90.dp) - .onFocusChanged { - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - fobColor = if (it.isFocused) hoverColor else secondaryColor + } + val scanLauncher = + rememberLauncherForActivityResult( + contract = ScanContract(), + onResult = { + if (it.contents != null) { + scope.launch { + viewModel.onTunnelQrResult(it.contents).let { result -> + when (result) { + is Result.Success -> {} + is Result.Error -> showSnackbarMessage(result.error.message) } - }, - onClick = { showBottomSheet = true }, - containerColor = fobColor, - shape = RoundedCornerShape(16.dp)) { + } + } + } + }, + ) + + AnimatedVisibility(showPrimaryChangeAlertDialog) { + AlertDialog( + onDismissRequest = { showPrimaryChangeAlertDialog = false }, + confirmButton = { + TextButton( + onClick = { + viewModel.onDefaultTunnelChange(selectedTunnel) + showPrimaryChangeAlertDialog = false + selectedTunnel = null + }, + ) { + Text(text = stringResource(R.string.okay)) + } + }, + dismissButton = { + TextButton(onClick = { showPrimaryChangeAlertDialog = false }) { + Text(text = stringResource(R.string.cancel)) + } + }, + title = { Text(text = stringResource(R.string.primary_tunnel_change)) }, + text = { Text(text = stringResource(R.string.primary_tunnel_change_question)) }, + ) + } + + AnimatedVisibility(showDeleteTunnelAlertDialog) { + AlertDialog( + onDismissRequest = { showDeleteTunnelAlertDialog = false }, + confirmButton = { + TextButton( + onClick = { + selectedTunnel?.let { viewModel.onDelete(it) } + showDeleteTunnelAlertDialog = false + selectedTunnel = null + }, + ) { + Text(text = stringResource(R.string.yes)) + } + }, + dismissButton = { + TextButton(onClick = { showDeleteTunnelAlertDialog = false }) { + Text(text = stringResource(R.string.cancel)) + } + }, + title = { Text(text = stringResource(R.string.delete_tunnel)) }, + text = { Text(text = stringResource(R.string.delete_tunnel_message)) }, + ) + } + + fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) { + if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop() + } + + Scaffold( + modifier = + Modifier.pointerInput(Unit) { + detectTapGestures( + onTap = { + if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) selectedTunnel = null + }, + ) + }, + floatingActionButtonPosition = FabPosition.End, + topBar = { + if (uiState.settings.isAutoTunnelEnabled) + TopAppBar( + title = { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier.requiredWidth(LocalConfiguration.current.screenWidthDp.dp) + .padding(end = 5.dp), + ) { + Row { + Icon( + Icons.Rounded.Bolt, + stringResource(id = R.string.auto), + modifier = Modifier.size(25.dp), + tint = + if (uiState.settings.isAutoTunnelPaused) Color.Gray + else mint, + ) + Text( + "Auto-tunneling: ${if (uiState.settings.isAutoTunnelPaused) "paused" else "active"}", + style = typography.bodyLarge, + modifier = Modifier.padding(start = 10.dp), + ) + } + if (uiState.settings.isAutoTunnelPaused) + TextButton( + onClick = { viewModel.resumeAutoTunneling() }, + modifier = Modifier.padding(end = 10.dp), + ) { + Text("Resume") + } + else + TextButton( + onClick = { viewModel.pauseAutoTunneling() }, + modifier = Modifier.padding(end = 10.dp), + ) { + Text("Pause") + } + } + }, + ) + }, + floatingActionButton = { + AnimatedVisibility( + visible = isVisible.value, + enter = slideInVertically(initialOffsetY = { it * 2 }), + exit = slideOutVertically(targetOffsetY = { it * 2 }), + ) { + val secondaryColor = MaterialTheme.colorScheme.secondary + val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) + var fobColor by remember { mutableStateOf(secondaryColor) } + FloatingActionButton( + modifier = + (if ( + WireGuardAutoTunnel.isRunningOnAndroidTv() && + uiState.tunnels.isEmpty() + ) + Modifier.focusRequester(focusRequester) + else Modifier) + .padding(bottom = 90.dp) + .onFocusChanged { + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + fobColor = if (it.isFocused) hoverColor else secondaryColor + } + }, + onClick = { showBottomSheet = true }, + containerColor = fobColor, + shape = RoundedCornerShape(16.dp), + ) { Icon( 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)) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxSize().padding(padding), + ) { Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic) - } + } } if (showBottomSheet) { - ModalBottomSheet( - onDismissRequest = { showBottomSheet = false }, sheetState = sheetState) { + ModalBottomSheet( + onDismissRequest = { showBottomSheet = false }, + sheetState = sheetState, + ) { // Sheet content Row( modifier = Modifier.fillMaxWidth() .clickable { - showBottomSheet = false - tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES) + showBottomSheet = false + tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES) } - .padding(10.dp)) { - Icon( - Icons.Filled.FileOpen, - contentDescription = stringResource(id = R.string.open_file), - modifier = Modifier.padding(10.dp)) - Text( - stringResource(id = R.string.add_tunnels_text), - modifier = Modifier.padding(10.dp)) - } + .padding(10.dp), + ) { + Icon( + Icons.Filled.FileOpen, + contentDescription = stringResource(id = R.string.open_file), + modifier = Modifier.padding(10.dp), + ) + Text( + stringResource(id = R.string.add_tunnels_text), + modifier = Modifier.padding(10.dp), + ) + } if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) { - Divider() - Row( - modifier = - Modifier.fillMaxWidth() - .clickable { - scope.launch { - showBottomSheet = false - val scanOptions = ScanOptions() - scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE) - scanOptions.setOrientationLocked(true) - scanOptions.setPrompt(context.getString(R.string.scanning_qr)) - scanOptions.setBeepEnabled(false) - scanOptions.captureActivity = CaptureActivityPortrait::class.java - scanLauncher.launch(scanOptions) + Divider() + Row( + modifier = + Modifier.fillMaxWidth() + .clickable { + scope.launch { + showBottomSheet = false + val scanOptions = ScanOptions() + scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE) + scanOptions.setOrientationLocked(true) + scanOptions.setPrompt( + context.getString(R.string.scanning_qr) + ) + scanOptions.setBeepEnabled(false) + 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() Row( modifier = Modifier.fillMaxWidth() .clickable { - showBottomSheet = false - navController.navigate( - "${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}") + showBottomSheet = false + navController.navigate( + "${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}", + ) } - .padding(10.dp)) { - Icon( - Icons.Filled.Create, - contentDescription = stringResource(id = R.string.create_import), - modifier = Modifier.padding(10.dp)) - Text( - stringResource(id = R.string.create_import), - modifier = Modifier.padding(10.dp)) - } - } + .padding(10.dp), + ) { + Icon( + Icons.Filled.Create, + contentDescription = stringResource(id = R.string.create_import), + modifier = Modifier.padding(10.dp), + ) + Text( + stringResource(id = R.string.create_import), + modifier = Modifier.padding(10.dp), + ) + } + } } LazyColumn( 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 -> - val leadingIconColor = - (if (uiState.vpnState.name == tunnel.name && - uiState.vpnState.status == Tunnel.State.UP) { - uiState.vpnState.statistics - ?.mapPeerStats() - ?.map { it.value?.handshakeStatus() } - .let { statuses -> + 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 + ) { + uiState.vpnState.statistics + ?.mapPeerStats() + ?.map { it.value?.handshakeStatus() } + .let { statuses -> when { - statuses?.all { it == HandshakeStatus.HEALTHY } == true -> mint - statuses?.any { it == HandshakeStatus.STALE } == true -> corn - statuses?.all { it == HandshakeStatus.NOT_STARTED } == true -> - Color.Gray - else -> { - Color.Gray - } + statuses?.all { it == HandshakeStatus.HEALTHY } == true -> mint + statuses?.any { it == HandshakeStatus.STALE } == true -> corn + statuses?.all { it == HandshakeStatus.NOT_STARTED } == true -> + Color.Gray + else -> { + Color.Gray + } } - } - } else { - Color.Gray - }) - val expanded = remember { mutableStateOf(false) } - RowListItem( - icon = { - if (uiState.settings.isTunnelConfigDefault(tunnel)) { + } + } else { + Color.Gray + }) + val expanded = remember { mutableStateOf(false) } + RowListItem( + icon = { + if (uiState.settings.isTunnelConfigDefault(tunnel)) { Icon( Icons.Rounded.Star, stringResource(R.string.status), tint = leadingIconColor, - modifier = Modifier.padding(end = 10.dp).size(20.dp)) - } else { + 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)) - } - }, - text = tunnel.name, - onHold = { - if ((uiState.vpnState.status == Tunnel.State.UP) && - (tunnel.name == uiState.vpnState.name)) { + 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) + ) { showSnackbarMessage(Event.Message.TunnelOffAction.message) return@RowListItem - } - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - selectedTunnel = tunnel - }, - onClick = { - if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) { - if (uiState.vpnState.status == Tunnel.State.UP && - (uiState.vpnState.name == tunnel.name)) { - expanded.value = !expanded.value + } + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + selectedTunnel = tunnel + }, + onClick = { + if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) { + if ( + uiState.vpnState.status == Tunnel.State.UP && + (uiState.vpnState.name == tunnel.name) + ) { + expanded.value = !expanded.value } - } else { + } else { selectedTunnel = tunnel focusRequester.requestFocus() - } - }, - statistics = uiState.vpnState.statistics, - expanded = expanded.value, - rowButton = { - if (tunnel.id == selectedTunnel?.id && - !WireGuardAutoTunnel.isRunningOnAndroidTv()) { + } + }, + statistics = uiState.vpnState.statistics, + expanded = expanded.value, + rowButton = { + if ( + tunnel.id == selectedTunnel?.id && + !WireGuardAutoTunnel.isRunningOnAndroidTv() + ) { Row { - if (!uiState.settings.isTunnelConfigDefault(tunnel)) { + if (!uiState.settings.isTunnelConfigDefault(tunnel)) { + IconButton( + onClick = { + if ( + uiState.settings.isAutoTunnelEnabled && + !uiState.settings.isAutoTunnelPaused + ) { + showSnackbarMessage( + Event.Message.AutoTunnelOffAction.message, + ) + } else { + showPrimaryChangeAlertDialog = true + } + }, + ) { + Icon( + Icons.Rounded.Star, + stringResource(id = R.string.set_primary), + ) + } + } IconButton( onClick = { - if (uiState.settings.isAutoTunnelEnabled && !uiState.settings.isAutoTunnelPaused) { - showSnackbarMessage( - Event.Message.AutoTunnelOffAction.message) - } else { - showPrimaryChangeAlertDialog = true - } - }) { - Icon( - Icons.Rounded.Star, - stringResource(id = R.string.set_primary)) - } - } - IconButton( - onClick = { - if (uiState.settings.isAutoTunnelEnabled && uiState.settings.isTunnelConfigDefault(tunnel) - && !uiState.settings.isAutoTunnelPaused) { - showSnackbarMessage( - Event.Message.AutoTunnelOffAction.message) - } else navController.navigate( - "${Screen.Config.route}/${selectedTunnel?.id}") - }) { + if ( + uiState.settings.isAutoTunnelEnabled && + uiState.settings.isTunnelConfigDefault( + tunnel, + ) && + !uiState.settings.isAutoTunnelPaused + ) { + showSnackbarMessage( + Event.Message.AutoTunnelOffAction.message, + ) + } else + navController.navigate( + "${Screen.Config.route}/${selectedTunnel?.id}", + ) + }, + ) { Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit)) - } - IconButton( - modifier = Modifier.focusable(), - onClick = { viewModel.onDelete(tunnel) }) { + } + IconButton( + modifier = Modifier.focusable(), + onClick = { showDeleteTunnelAlertDialog = true }, + ) { Icon(Icons.Rounded.Delete, stringResource(id = R.string.delete)) - } + } } - } else { + } else { val checked by remember { - derivedStateOf { - (uiState.vpnState.status == Tunnel.State.UP && - tunnel.name == uiState.vpnState.name) - } + derivedStateOf { + (uiState.vpnState.status == Tunnel.State.UP && + tunnel.name == uiState.vpnState.name) + } } if (!checked) expanded.value = false @@ -491,72 +594,94 @@ fun MainScreen( modifier = Modifier.focusRequester(focusRequester), checked = checked, onCheckedChange = { checked -> - if (!checked) expanded.value = false - onTunnelToggle(checked, tunnel) - }) + if (!checked) expanded.value = false + onTunnelToggle(checked, tunnel) + }, + ) if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - Row { - if (!uiState.settings.isTunnelConfigDefault(tunnel)) { - IconButton( - onClick = { - if (uiState.settings.isAutoTunnelEnabled) { - showSnackbarMessage( - Event.Message.AutoTunnelOffAction.message) - } else { - selectedTunnel = tunnel - showPrimaryChangeAlertDialog = true + Row { + if (!uiState.settings.isTunnelConfigDefault(tunnel)) { + IconButton( + onClick = { + if (uiState.settings.isAutoTunnelEnabled) { + showSnackbarMessage( + Event.Message.AutoTunnelOffAction.message, + ) + } else { + selectedTunnel = tunnel + showPrimaryChangeAlertDialog = true + } + }, + ) { + Icon( + Icons.Rounded.Star, + stringResource(id = R.string.set_primary), + ) } - }) { + } + IconButton( + modifier = Modifier.focusRequester(focusRequester), + onClick = { + if ( + uiState.vpnState.status == Tunnel.State.UP && + (uiState.vpnState.name == tunnel.name) + ) { + expanded.value = !expanded.value + } else { + showSnackbarMessage( + Event.Message.TunnelOnAction.message + ) + } + }, + ) { + Icon(Icons.Rounded.Info, stringResource(R.string.info)) + } + IconButton( + onClick = { + if ( + uiState.vpnState.status == Tunnel.State.UP && + tunnel.name == uiState.vpnState.name + ) { + showSnackbarMessage( + Event.Message.TunnelOffAction.message + ) + } else { + navController.navigate( + "${Screen.Config.route}/${tunnel.id}", + ) + } + }, + ) { + Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit)) + } + IconButton( + onClick = { + if ( + uiState.vpnState.status == Tunnel.State.UP && + tunnel.name == uiState.vpnState.name + ) { + showSnackbarMessage( + Event.Message.TunnelOffAction.message + ) + } else { + showDeleteTunnelAlertDialog = true + } + }, + ) { Icon( - Icons.Rounded.Star, - stringResource(id = R.string.set_primary)) - } + Icons.Rounded.Delete, + stringResource(id = R.string.delete), + ) + } + TunnelSwitch() } - IconButton( - modifier = Modifier.focusRequester(focusRequester), - onClick = { - if (uiState.vpnState.status == Tunnel.State.UP && - (uiState.vpnState.name == tunnel.name)) { - expanded.value = !expanded.value - } else { - showSnackbarMessage(Event.Message.TunnelOnAction.message) - } - }) { - Icon(Icons.Rounded.Info, stringResource(R.string.info)) - } - IconButton( - onClick = { - if (uiState.vpnState.status == Tunnel.State.UP && - tunnel.name == uiState.vpnState.name) { - showSnackbarMessage(Event.Message.TunnelOffAction.message) - } else { - navController.navigate( - "${Screen.Config.route}/${tunnel.id}") - } - }) { - Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit)) - } - IconButton( - onClick = { - if (uiState.vpnState.status == Tunnel.State.UP && - tunnel.name == uiState.vpnState.name) { - showSnackbarMessage(Event.Message.TunnelOffAction.message) - } else { - viewModel.onDelete(tunnel) - } - }) { - Icon( - Icons.Rounded.Delete, - stringResource(id = R.string.delete)) - } - TunnelSwitch() - } } else { - TunnelSwitch() + TunnelSwitch() } - } - }) - } + } + }, + ) } - } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainUiState.kt index 3246a25..775bbba 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainUiState.kt @@ -5,8 +5,8 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs data class MainUiState( - val settings : Settings = Settings(), - val tunnels : TunnelConfigs = emptyList(), + val settings: Settings = Settings(), + val tunnels: TunnelConfigs = emptyList(), val vpnState: VpnState = VpnState(), - val loading : Boolean = true + val loading: Boolean = true ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt index 9965d12..5008ccd 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt @@ -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 @@ -46,216 +43,225 @@ constructor( private val vpnService: VpnService ) : ViewModel() { - val uiState = - combine( - settingsRepository.getSettingsFlow(), - tunnelConfigRepository.getTunnelConfigsFlow(), - vpnService.vpnState, - ) { settings, tunnels, vpnState -> - validateWatcherServiceState(settings) - MainUiState(settings, tunnels, vpnState, false) - } - .stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), - MainUiState()) + val uiState = + combine( + settingsRepository.getSettingsFlow(), + tunnelConfigRepository.getTunnelConfigsFlow(), + vpnService.vpnState, + ) { settings, tunnels, vpnState -> + validateWatcherServiceState(settings) + MainUiState(settings, tunnels, vpnState, false) + } + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), + 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) { - ServiceManager.startWatcherService(application.applicationContext) - } - } - - private fun stopWatcherService() = viewModelScope.launch(Dispatchers.IO) { - ServiceManager.stopWatcherService(application.applicationContext) - } - fun onDelete(tunnel: TunnelConfig) { - viewModelScope.launch(Dispatchers.IO) { - if (tunnelConfigRepository.count() == 1) { - stopWatcherService() - val settings = settingsRepository.getSettings() - settings.defaultTunnel = null - settings.isAutoTunnelEnabled = false - settings.isAlwaysOnVpnEnabled = false - saveSettings(settings) - } - tunnelConfigRepository.delete(tunnel) - WireGuardAutoTunnel.requestTileServiceStateUpdate() - } - } - - fun onTunnelStart(tunnelConfig: TunnelConfig) = viewModelScope.launch(Dispatchers.IO) { - stopActiveTunnel().await() - startTunnel(tunnelConfig) - } - - private fun startTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch(Dispatchers.IO) { - ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString()) - } - - private fun stopActiveTunnel() = - viewModelScope.async(Dispatchers.IO) { - if (ServiceManager.getServiceState( - application.applicationContext, WireGuardTunnelService::class.java) == - ServiceState.STARTED) { - onTunnelStop() - delay(Constants.TOGGLE_TUNNEL_DELAY) + private fun validateWatcherServiceState(settings: Settings) = + viewModelScope.launch(Dispatchers.IO) { + if (settings.isAutoTunnelEnabled) { + ServiceManager.startWatcherService(application.applicationContext) + } } - } - fun onTunnelStop() = viewModelScope.launch(Dispatchers.IO) { - ServiceManager.stopVpnService(application.applicationContext) - } + private fun stopWatcherService() = + viewModelScope.launch(Dispatchers.IO) { + ServiceManager.stopWatcherService(application.applicationContext) + } - private fun validateConfigString(config: String) { - TunnelConfig.configFromQuick(config) - } + fun onDelete(tunnel: TunnelConfig) { + viewModelScope.launch(Dispatchers.IO) { + if (tunnelConfigRepository.count() == 1) { + stopWatcherService() + val settings = settingsRepository.getSettings() + settings.defaultTunnel = null + settings.isAutoTunnelEnabled = false + settings.isAlwaysOnVpnEnabled = false + saveSettings(settings) + } + tunnelConfigRepository.delete(tunnel) + WireGuardAutoTunnel.requestTileServiceStateUpdate() + } + } - suspend fun onTunnelQrResult(result: String) : Result { + fun onTunnelStart(tunnelConfig: TunnelConfig) = + viewModelScope.launch(Dispatchers.IO) { + Timber.d("On start called!") + stopActiveTunnel().await() + startTunnel(tunnelConfig) + } + + private fun startTunnel(tunnelConfig: TunnelConfig) = + viewModelScope.launch(Dispatchers.IO) { + Timber.d("Start tunnel via manager") + ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString()) + } + + private fun stopActiveTunnel() = + viewModelScope.async(Dispatchers.IO) { + onTunnelStop() + delay(Constants.TOGGLE_TUNNEL_DELAY) + } + + fun onTunnelStop() = + viewModelScope.launch(Dispatchers.IO) { + Timber.d("Stopping active tunnel") + ServiceManager.stopVpnService(application.applicationContext) + } + + private fun validateConfigString(config: String) { + TunnelConfig.configFromQuick(config) + } + + suspend fun onTunnelQrResult(result: String): Result { return try { - validateConfigString(result) - val tunnelConfig = - TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result) - addTunnel(tunnelConfig) + validateConfigString(result) + val tunnelConfig = + TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result) + addTunnel(tunnelConfig) Result.Success(Unit) } catch (e: Exception) { Result.Error(Event.Error.InvalidQrCode) } - } + } - private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) { - val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8) - val config = Config.parse(bufferReader) - val tunnelName = getNameFromFileName(fileName) - addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString())) - withContext(Dispatchers.IO) { stream.close() } - } + private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) { + val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8) + val config = Config.parse(bufferReader) + val tunnelName = getNameFromFileName(fileName) + addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString())) + withContext(Dispatchers.IO) { stream.close() } + } - private fun getInputStreamFromUri(uri: Uri): InputStream? { - return application.applicationContext.contentResolver.openInputStream(uri) - } + private fun getInputStreamFromUri(uri: Uri): InputStream? { + return application.applicationContext.contentResolver.openInputStream(uri) + } - suspend fun onTunnelFileSelected(uri: Uri) : Result { + suspend fun onTunnelFileSelected(uri: Uri): Result { try { - if(isValidUriContentScheme(uri)){ + if (isValidUriContentScheme(uri)) { val fileName = getFileName(application.applicationContext, uri) when (getFileExtensionFromFileName(fileName)) { - Constants.CONF_FILE_EXTENSION -> saveTunnelFromConfUri(fileName, uri).let { - when(it) { - is Result.Error -> return Result.Error(Event.Error.FileReadFailed) - is Result.Success -> return it + Constants.CONF_FILE_EXTENSION -> + saveTunnelFromConfUri(fileName, uri).let { + when (it) { + is Result.Error -> return Result.Error(Event.Error.FileReadFailed) + is Result.Success -> return it + } } - } Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri) else -> return Result.Error(Event.Error.InvalidFileExtension) } return Result.Success(Unit) - } else { - return Result.Error(Event.Error.InvalidFileExtension) + } else { + return Result.Error(Event.Error.InvalidFileExtension) } } catch (e: Exception) { return Result.Error(Event.Error.FileReadFailed) } - } - - private suspend fun saveTunnelsFromZipUri(uri: Uri) { - ZipInputStream(getInputStreamFromUri(uri)).use { zip -> - generateSequence { zip.nextEntry } - .filterNot { - it.isDirectory || getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION - } - .forEach { - val name = getNameFromFileName(it.name) - val config = Config.parse(zip) - viewModelScope.launch(Dispatchers.IO) { - addTunnel(TunnelConfig(name = name, wgQuick = config.toWgQuickString())) - } - } - } - } - - private suspend fun saveTunnelFromConfUri(name: String, uri: Uri) : Result { - val stream = getInputStreamFromUri(uri) - return if(stream != null) { - saveTunnelConfigFromStream(stream, name) - Result.Success(Unit) - } else { - Result.Error(Event.Error.FileReadFailed) - } - } - - private suspend fun addTunnel(tunnelConfig: TunnelConfig) { - saveTunnel(tunnelConfig) - WireGuardAutoTunnel.requestTileServiceStateUpdate() - } - - fun pauseAutoTunneling() = viewModelScope.launch { - settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = true)) } - fun resumeAutoTunneling() = viewModelScope.launch { - settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = false)) + private suspend fun saveTunnelsFromZipUri(uri: Uri) { + ZipInputStream(getInputStreamFromUri(uri)).use { zip -> + generateSequence { zip.nextEntry } + .filterNot { + it.isDirectory || + getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION + } + .forEach { + val name = getNameFromFileName(it.name) + val config = Config.parse(zip) + viewModelScope.launch(Dispatchers.IO) { + addTunnel(TunnelConfig(name = name, wgQuick = config.toWgQuickString())) + } + } + } } - private suspend fun saveTunnel(tunnelConfig: TunnelConfig) { - tunnelConfigRepository.save(tunnelConfig) - } - - private fun getFileNameByCursor(context: Context, uri: Uri): String? { - context.contentResolver.query(uri, null, null, null, null)?.use { - return getDisplayNameByCursor(it) + private suspend fun saveTunnelFromConfUri(name: String, uri: Uri): Result { + val stream = getInputStreamFromUri(uri) + return if (stream != null) { + saveTunnelConfigFromStream(stream, name) + Result.Success(Unit) + } else { + Result.Error(Event.Error.FileReadFailed) + } } - return null - } - private fun getDisplayNameColumnIndex(cursor: Cursor): Int? { - val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) - return if (columnIndex != -1) { - return columnIndex - } else { - null + private suspend fun addTunnel(tunnelConfig: TunnelConfig) { + saveTunnel(tunnelConfig) + WireGuardAutoTunnel.requestTileServiceStateUpdate() } - } - private fun getDisplayNameByCursor(cursor: Cursor): String? { - return if (cursor.moveToFirst()) { - val index = getDisplayNameColumnIndex(cursor) - if (index != null) { - cursor.getString(index) - } else null - } else null - } + fun pauseAutoTunneling() = + viewModelScope.launch { + settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = true)) + } - private fun isValidUriContentScheme(uri: Uri): Boolean { - return uri.scheme == Constants.URI_CONTENT_SCHEME - } - private fun getFileName(context: Context, uri: Uri): String { + fun resumeAutoTunneling() = + viewModelScope.launch { + settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = false)) + } + + private suspend fun saveTunnel(tunnelConfig: TunnelConfig) { + tunnelConfigRepository.save(tunnelConfig) + } + + private fun getFileNameByCursor(context: Context, uri: Uri): String? { + context.contentResolver.query(uri, null, null, null, null)?.use { + return getDisplayNameByCursor(it) + } + return null + } + + private fun getDisplayNameColumnIndex(cursor: Cursor): Int? { + val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + return if (columnIndex != -1) { + return columnIndex + } else { + null + } + } + + private fun getDisplayNameByCursor(cursor: Cursor): String? { + return if (cursor.moveToFirst()) { + val index = getDisplayNameColumnIndex(cursor) + if (index != null) { + cursor.getString(index) + } else null + } else null + } + + private fun isValidUriContentScheme(uri: Uri): Boolean { + return uri.scheme == Constants.URI_CONTENT_SCHEME + } + + private fun getFileName(context: Context, uri: Uri): String { return getFileNameByCursor(context, uri) ?: NumberUtils.generateRandomTunnelName() - } - - private fun getNameFromFileName(fileName: String): String { - return fileName.substring(0, fileName.lastIndexOf('.')) - } - - private fun getFileExtensionFromFileName(fileName: String): String { - return try { - fileName.substring(fileName.lastIndexOf('.')) - } catch (e: Exception) { - "" } - } - private fun saveSettings(settings: Settings) = - viewModelScope.launch(Dispatchers.IO) { settingsRepository.save(settings) } - - fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) = viewModelScope.launch { - if (selectedTunnel != null) { - saveSettings(uiState.value.settings.copy(defaultTunnel = selectedTunnel.toString())).join() - WireGuardAutoTunnel.requestTileServiceStateUpdate() + private fun getNameFromFileName(fileName: String): String { + return fileName.substring(0, fileName.lastIndexOf('.')) } - } + + private fun getFileExtensionFromFileName(fileName: String): String { + return try { + fileName.substring(fileName.lastIndexOf('.')) + } catch (e: Exception) { + "" + } + } + + private fun saveSettings(settings: Settings) = + viewModelScope.launch(Dispatchers.IO) { settingsRepository.save(settings) } + + fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) = + viewModelScope.launch { + if (selectedTunnel != null) { + saveSettings(uiState.value.settings.copy(defaultTunnel = selectedTunnel.toString())) + .join() + WireGuardAutoTunnel.requestTileServiceStateUpdate() + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt index f5d053d..6ad7393 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt @@ -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(), @@ -91,92 +99,127 @@ fun SettingsScreen( showSnackbarMessage: (String) -> Unit, focusRequester: FocusRequester ) { - val scope = rememberCoroutineScope { Dispatchers.IO } - val context = LocalContext.current - val focusManager = LocalFocusManager.current - val scrollState = rememberScrollState() - val interactionSource = remember { MutableInteractionSource() } + val scope = rememberCoroutineScope { Dispatchers.IO } + val context = LocalContext.current + val focusManager = LocalFocusManager.current + val scrollState = rememberScrollState() + val interactionSource = remember { MutableInteractionSource() } - val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION) - var currentText by remember { mutableStateOf("") } - var isBackgroundLocationGranted by remember { mutableStateOf(true) } + val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION) + var currentText by remember { mutableStateOf("") } + var isBackgroundLocationGranted by remember { mutableStateOf(true) } var showLocationServicesAlertDialog by remember { mutableStateOf(false) } - var didExportFiles by remember { mutableStateOf(false) } - var showAuthPrompt by remember { mutableStateOf(false) } + var didExportFiles by remember { mutableStateOf(false) } + var showAuthPrompt by remember { mutableStateOf(false) } val focusRequester2 = remember { FocusRequester() } - val screenPadding = 5.dp - val fillMaxWidth = .85f + val screenPadding = 5.dp + val fillMaxWidth = .85f - if (uiState.loading) { - LoadingScreen() - return - } - - fun exportAllConfigs() { - try { - val files = uiState.tunnels.map { File(context.cacheDir, "${it.name}.conf") } - files.forEachIndexed { index, file -> - file.outputStream().use { it.write(uiState.tunnels[index].wgQuick.toByteArray()) } - } - FileUtils.saveFilesToZip(context, files) - didExportFiles = true - showSnackbarMessage(Event.Message.ConfigsExported.message) - } catch (e: Exception) { - showSnackbarMessage(Event.Error.Exception(e).message) + if (uiState.loading) { + LoadingScreen() + return } - } - fun saveTrustedSSID() { - if (currentText.isNotEmpty()) { - viewModel.onSaveTrustedSSID(currentText).let { - when(it) { - is Result.Success -> currentText = "" - is Result.Error -> showSnackbarMessage(it.error.message) - } - } - } - } - - fun openSettings() { - scope.launch { - val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS) - intentSettings.data = Uri.fromParts("package", context.packageName, null) - context.startActivity(intentSettings) - } - } - - fun checkFineLocationGranted() { - isBackgroundLocationGranted = - if (!fineLocationState.status.isGranted) { - false - } else { - viewModel.setLocationDisclosureShown() - true + val startForResult = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { + result: ActivityResult -> + if (result.resultCode == Activity.RESULT_OK) { + val intent = result.data + // Handle the Intent + } + viewModel.setBatteryOptimizeDisableShown() } - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - if(WireGuardAutoTunnel.isRunningOnAndroidTv() && Build.VERSION.SDK_INT == Build.VERSION_CODES.Q){ - checkFineLocationGranted() - } else { - val backgroundLocationState = - rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION) - isBackgroundLocationGranted = - if (!backgroundLocationState.status.isGranted) { - false - } else { - SideEffect { viewModel.setLocationDisclosureShown() } - true - } - } - } + fun exportAllConfigs() { + try { + val files = uiState.tunnels.map { File(context.cacheDir, "${it.name}.conf") } + files.forEachIndexed { index, file -> + file.outputStream().use { it.write(uiState.tunnels[index].wgQuick.toByteArray()) } + } + FileUtils.saveFilesToZip(context, files) + didExportFiles = true + showSnackbarMessage(Event.Message.ConfigsExported.message) + } catch (e: Exception) { + showSnackbarMessage(Event.Error.Exception(e).message) + } + } - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { - checkFineLocationGranted() - } + 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 { + when (it) { + is Result.Success -> currentText = "" + is Result.Error -> showSnackbarMessage(it.error.message) + } + } + } + } + + fun openSettings() { + scope.launch { + val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS) + intentSettings.data = Uri.fromParts("package", context.packageName, null) + context.startActivity(intentSettings) + } + } + + fun checkFineLocationGranted() { + isBackgroundLocationGranted = + if (!fineLocationState.status.isGranted) { + false + } else { + viewModel.setLocationDisclosureShown() + true + } + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if ( + WireGuardAutoTunnel.isRunningOnAndroidTv() && + Build.VERSION.SDK_INT == Build.VERSION_CODES.Q + ) { + checkFineLocationGranted() + } else { + val backgroundLocationState = + rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION) + isBackgroundLocationGranted = + if (!backgroundLocationState.status.isGranted) { + false + } else { + SideEffect { viewModel.setLocationDisclosureShown() } + true + } + } + } + + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { + checkFineLocationGranted() + } AnimatedVisibility(showLocationServicesAlertDialog) { AlertDialog( @@ -185,8 +228,9 @@ fun SettingsScreen( TextButton( onClick = { showLocationServicesAlertDialog = false - viewModel.toggleAutoTunnel() - }) { + handleAutoTunnelToggle() + }, + ) { Text(text = stringResource(R.string.okay)) } }, @@ -196,253 +240,313 @@ 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)) { - Icon( - Icons.Rounded.LocationOff, - contentDescription = stringResource(id = R.string.map), - 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) - Text( - stringResource(R.string.prominent_background_location_message), - textAlign = TextAlign.Center, - modifier = Modifier.padding(30.dp), - fontSize = 15.sp) - Row( - modifier = - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - Modifier.fillMaxWidth().padding(10.dp) - } else { - Modifier.fillMaxWidth().padding(30.dp) - }, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceEvenly) { + if (!uiState.isLocationDisclosureShown) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + 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), + ) + Text( + stringResource(R.string.prominent_background_location_title), + textAlign = TextAlign.Center, + modifier = Modifier.padding(30.dp), + fontSize = 20.sp, + ) + Text( + stringResource(R.string.prominent_background_location_message), + textAlign = TextAlign.Center, + modifier = Modifier.padding(30.dp), + fontSize = 15.sp, + ) + Row( + modifier = + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + Modifier.fillMaxWidth().padding(10.dp) + } else { + Modifier.fillMaxWidth().padding(30.dp) + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly, + ) { TextButton(onClick = { viewModel.setLocationDisclosureShown() }) { - Text(stringResource(id = R.string.no_thanks)) + Text(stringResource(id = R.string.no_thanks)) } TextButton( modifier = Modifier.focusRequester(focusRequester), onClick = { - openSettings() - viewModel.setLocationDisclosureShown() - }) { - Text(stringResource(id = R.string.turn_on)) - } - } + openSettings() + viewModel.setLocationDisclosureShown() + }, + ) { + Text(stringResource(id = R.string.turn_on)) + } + } } - } + } - if(showAuthPrompt) { - AuthorizationPrompt( - onSuccess = { - showAuthPrompt = false - exportAllConfigs() - }, - onError = { _ -> - showAuthPrompt = false - showSnackbarMessage(Event.Error.AuthenticationFailed.message) - }, - onFailure = { - showAuthPrompt = false - showSnackbarMessage(Event.Error.AuthorizationFailed.message) - }) - } + if (showAuthPrompt) { + AuthorizationPrompt( + onSuccess = { + showAuthPrompt = false + exportAllConfigs() + }, + onError = { _ -> + showAuthPrompt = false + showSnackbarMessage(Event.Error.AuthenticationFailed.message) + }, + 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)) { - Text( - stringResource(R.string.one_tunnel_required), - textAlign = TextAlign.Center, - modifier = Modifier.padding(15.dp), - fontStyle = FontStyle.Italic) + if (uiState.tunnels.isEmpty() && uiState.isLocationDisclosureShown) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxSize().padding(padding), + ) { + Text( + stringResource(R.string.one_tunnel_required), + textAlign = TextAlign.Center, + modifier = Modifier.padding(15.dp), + fontStyle = FontStyle.Italic, + ) } - } - if (uiState.isLocationDisclosureShown && uiState.tunnels.isNotEmpty()) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top, - modifier = - Modifier.fillMaxSize().verticalScroll(scrollState).clickable( - indication = null, interactionSource = interactionSource) { - focusManager.clearFocus() - }) { - Surface( - tonalElevation = 2.dp, - shadowElevation = 2.dp, - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surface, - modifier = - (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - Modifier.height(IntrinsicSize.Min) - .fillMaxWidth(fillMaxWidth) - .padding(top = 10.dp) - } else { - Modifier.fillMaxWidth(fillMaxWidth).padding(top = 60.dp) - }) - .padding(bottom = 10.dp)) { - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Top, - modifier = Modifier.padding(15.dp)) { - SectionTitle( - title = stringResource(id = R.string.auto_tunneling), - padding = screenPadding) - ConfigurationToggle( - stringResource(id = R.string.tunnel_on_wifi), - enabled = - !(uiState.settings.isAutoTunnelEnabled || - uiState.settings.isAlwaysOnVpnEnabled), - checked = uiState.settings.isTunnelOnWifiEnabled, - padding = screenPadding, - onCheckChanged = { viewModel.onToggleTunnelOnWifi() }, - modifier = if(uiState.settings.isAutoTunnelEnabled) Modifier else Modifier.focusRequester(focusRequester).focusProperties { down = focusRequester2 }) - AnimatedVisibility(visible = uiState.settings.isTunnelOnWifiEnabled) { - Column { - FlowRow( - modifier = Modifier.padding(screenPadding).fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(5.dp)) { - uiState.settings.trustedNetworkSSIDs.forEach { ssid -> - ClickableIconButton( - onClick = { if(WireGuardAutoTunnel.isRunningOnAndroidTv()) { - viewModel.onDeleteTrustedSSID(ssid) - focusRequester2.requestFocus() - }}, - onIconClick = { viewModel.onDeleteTrustedSSID(ssid) }, - text = ssid, - icon = Icons.Filled.Close, - enabled = - !(uiState.settings.isAutoTunnelEnabled || - uiState.settings.isAlwaysOnVpnEnabled)) - } - if (uiState.settings.trustedNetworkSSIDs.isEmpty()) { - Text( - stringResource(R.string.none), - fontStyle = FontStyle.Italic, - color = Color.Gray) - } - } - OutlinedTextField( - enabled = - !(uiState.settings.isAutoTunnelEnabled || - uiState.settings.isAlwaysOnVpnEnabled), - value = currentText, - onValueChange = { currentText = it }, - label = { Text(stringResource(R.string.add_trusted_ssid)) }, - modifier = - Modifier.padding( - start = screenPadding, top = 5.dp, bottom = 10.dp) - .focusRequester(focusRequester2) - , - maxLines = 1, - keyboardOptions = - KeyboardOptions( - capitalization = KeyboardCapitalization.None, - imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }), - trailingIcon = { - if (currentText != "") { - IconButton(onClick = { saveTrustedSSID() }) { - Icon( - imageVector = Icons.Outlined.Add, - contentDescription = - if (currentText == "") { - stringResource( - id = R.string.trusted_ssid_empty_description) - } else { - stringResource( - id = R.string.trusted_ssid_value_description) - }, - tint = MaterialTheme.colorScheme.primary) - } - } - }) - } - } - ConfigurationToggle( - stringResource(R.string.tunnel_mobile_data), - enabled = - !(uiState.settings.isAutoTunnelEnabled || - uiState.settings.isAlwaysOnVpnEnabled), - checked = uiState.settings.isTunnelOnMobileDataEnabled, - padding = screenPadding, - onCheckChanged = { viewModel.onToggleTunnelOnMobileData() }) - ConfigurationToggle( - stringResource(id = R.string.tunnel_on_ethernet), - enabled = - !(uiState.settings.isAutoTunnelEnabled || - uiState.settings.isAlwaysOnVpnEnabled), - checked = uiState.settings.isTunnelOnEthernetEnabled, - padding = screenPadding, - onCheckChanged = { viewModel.onToggleTunnelOnEthernet() }) - ConfigurationToggle( - stringResource(R.string.battery_saver), - enabled = - !(uiState.settings.isAutoTunnelEnabled || - uiState.settings.isAlwaysOnVpnEnabled), - checked = uiState.settings.isBatterySaverEnabled, - padding = screenPadding, - onCheckChanged = { viewModel.onToggleBatterySaver() }) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = (if(!uiState.settings.isAutoTunnelEnabled) Modifier else Modifier.focusRequester(focusRequester)) - .fillMaxSize().padding(top = 5.dp), - horizontalArrangement = Arrangement.Center) { - TextButton( - enabled = !uiState.settings.isAlwaysOnVpnEnabled, - onClick = { - if (uiState.settings.isTunnelOnWifiEnabled && !uiState.settings.isAutoTunnelEnabled) { - when(false) { - isBackgroundLocationGranted -> - showSnackbarMessage(Event.Error.BackgroundLocationRequired.message) - fineLocationState.status.isGranted -> - showSnackbarMessage(Event.Error.PreciseLocationRequired.message) - viewModel.isLocationEnabled(context) -> - showLocationServicesAlertDialog = true - else -> { - viewModel.toggleAutoTunnel() - } - } - } else { - viewModel.toggleAutoTunnel() - } - }) { - val autoTunnelButtonText = - if (uiState.settings.isAutoTunnelEnabled) { - stringResource(R.string.disable_auto_tunnel) - } else { - stringResource(id = R.string.enable_auto_tunnel) - } - Text(autoTunnelButtonText) - } - } - } - } - if (WgQuickBackend.hasKernelSupport()) { + } + if (uiState.isLocationDisclosureShown && uiState.tunnels.isNotEmpty()) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + modifier = + Modifier.fillMaxSize().verticalScroll(scrollState).clickable( + indication = null, + interactionSource = interactionSource, + ) { + focusManager.clearFocus() + }, + ) { Surface( tonalElevation = 2.dp, shadowElevation = 2.dp, shape = RoundedCornerShape(12.dp), color = MaterialTheme.colorScheme.surface, - modifier = Modifier.fillMaxWidth(fillMaxWidth).padding(vertical = 10.dp)) { - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Top, - modifier = Modifier.padding(15.dp)) { + modifier = + (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + Modifier.height(IntrinsicSize.Min) + .fillMaxWidth(fillMaxWidth) + .padding(top = 10.dp) + } else { + Modifier.fillMaxWidth(fillMaxWidth).padding(top = 60.dp) + }) + .padding(bottom = 10.dp), + ) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier.padding(15.dp), + ) { + SectionTitle( + title = stringResource(id = R.string.auto_tunneling), + padding = screenPadding, + ) + ConfigurationToggle( + stringResource(id = R.string.tunnel_on_wifi), + enabled = + !(uiState.settings.isAutoTunnelEnabled || + uiState.settings.isAlwaysOnVpnEnabled), + checked = uiState.settings.isTunnelOnWifiEnabled, + padding = screenPadding, + onCheckChanged = { viewModel.onToggleTunnelOnWifi() }, + modifier = + if (uiState.settings.isAutoTunnelEnabled) Modifier + else + Modifier.focusRequester(focusRequester).focusProperties { + down = focusRequester2 + }, + ) + AnimatedVisibility(visible = uiState.settings.isTunnelOnWifiEnabled) { + Column { + FlowRow( + modifier = Modifier.padding(screenPadding).fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(5.dp), + ) { + uiState.settings.trustedNetworkSSIDs.forEach { ssid -> + ClickableIconButton( + onClick = { + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + viewModel.onDeleteTrustedSSID(ssid) + focusRequester2.requestFocus() + } + }, + onIconClick = { viewModel.onDeleteTrustedSSID(ssid) }, + text = ssid, + icon = Icons.Filled.Close, + enabled = + !(uiState.settings.isAutoTunnelEnabled || + uiState.settings.isAlwaysOnVpnEnabled), + ) + } + if (uiState.settings.trustedNetworkSSIDs.isEmpty()) { + Text( + stringResource(R.string.none), + fontStyle = FontStyle.Italic, + color = Color.Gray, + ) + } + } + OutlinedTextField( + enabled = + !(uiState.settings.isAutoTunnelEnabled || + uiState.settings.isAlwaysOnVpnEnabled), + value = currentText, + onValueChange = { currentText = it }, + label = { Text(stringResource(R.string.add_trusted_ssid)) }, + modifier = + Modifier.padding( + start = screenPadding, + top = 5.dp, + bottom = 10.dp, + ) + .focusRequester(focusRequester2), + maxLines = 1, + keyboardOptions = + KeyboardOptions( + capitalization = KeyboardCapitalization.None, + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }), + trailingIcon = { + if (currentText != "") { + IconButton(onClick = { saveTrustedSSID() }) { + Icon( + imageVector = Icons.Outlined.Add, + contentDescription = + if (currentText == "") { + stringResource( + id = + R.string + .trusted_ssid_empty_description, + ) + } else { + stringResource( + id = + R.string + .trusted_ssid_value_description, + ) + }, + tint = MaterialTheme.colorScheme.primary, + ) + } + } + }, + ) + } + } + ConfigurationToggle( + stringResource(R.string.tunnel_mobile_data), + enabled = + !(uiState.settings.isAutoTunnelEnabled || + uiState.settings.isAlwaysOnVpnEnabled), + checked = uiState.settings.isTunnelOnMobileDataEnabled, + padding = screenPadding, + onCheckChanged = { viewModel.onToggleTunnelOnMobileData() }, + ) + ConfigurationToggle( + stringResource(id = R.string.tunnel_on_ethernet), + enabled = + !(uiState.settings.isAutoTunnelEnabled || + uiState.settings.isAlwaysOnVpnEnabled), + checked = uiState.settings.isTunnelOnEthernetEnabled, + padding = screenPadding, + onCheckChanged = { viewModel.onToggleTunnelOnEthernet() }, + ) + ConfigurationToggle( + stringResource(R.string.battery_saver), + enabled = + !(uiState.settings.isAutoTunnelEnabled || + uiState.settings.isAlwaysOnVpnEnabled), + checked = uiState.settings.isBatterySaverEnabled, + padding = screenPadding, + onCheckChanged = { viewModel.onToggleBatterySaver() }, + ) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + (if (!uiState.settings.isAutoTunnelEnabled) Modifier + else + Modifier.focusRequester( + focusRequester, + )) + .fillMaxSize() + .padding(top = 5.dp), + horizontalArrangement = Arrangement.Center, + ) { + TextButton( + enabled = !uiState.settings.isAlwaysOnVpnEnabled, + onClick = { + if ( + uiState.settings.isTunnelOnWifiEnabled && + !uiState.settings.isAutoTunnelEnabled + ) { + when (false) { + isBackgroundLocationGranted -> + showSnackbarMessage( + Event.Error.BackgroundLocationRequired.message + ) + fineLocationState.status.isGranted -> + showSnackbarMessage( + Event.Error.PreciseLocationRequired.message + ) + viewModel.isLocationEnabled(context) -> + showLocationServicesAlertDialog = true + else -> { + handleAutoTunnelToggle() + } + } + } else { + handleAutoTunnelToggle() + } + }, + ) { + val autoTunnelButtonText = + if (uiState.settings.isAutoTunnelEnabled) { + stringResource(R.string.disable_auto_tunnel) + } else { + stringResource(id = R.string.enable_auto_tunnel) + } + Text(autoTunnelButtonText) + } + } + } + } + if (WgQuickBackend.hasKernelSupport()) { + Surface( + tonalElevation = 2.dp, + shadowElevation = 2.dp, + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surface, + modifier = Modifier.fillMaxWidth(fillMaxWidth).padding(vertical = 10.dp), + ) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier.padding(15.dp), + ) { SectionTitle( - title = stringResource(id = R.string.kernel), padding = screenPadding) + title = stringResource(id = R.string.kernel), + padding = screenPadding, + ) ConfigurationToggle( stringResource(R.string.use_kernel), enabled = @@ -451,58 +555,70 @@ fun SettingsScreen( (uiState.vpnState.status == Tunnel.State.UP)), checked = uiState.settings.isKernelEnabled, padding = screenPadding, - onCheckChanged = { viewModel.onToggleKernelMode().let { - when(it) { - is Result.Error -> showSnackbarMessage(it.error.message) - is Result.Success -> {} + onCheckChanged = { + viewModel.onToggleKernelMode().let { + when (it) { + is Result.Error -> showSnackbarMessage(it.error.message) + is Result.Success -> {} + } } - } }) - } + }, + ) + } } - } - if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) { - Surface( - tonalElevation = 2.dp, - shadowElevation = 2.dp, - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surface, - modifier = - Modifier.fillMaxWidth(fillMaxWidth) - .padding(vertical = 10.dp) - .padding(bottom = 140.dp)) { - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Top, - modifier = Modifier.padding(15.dp)) { + } + if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) { + Surface( + tonalElevation = 2.dp, + shadowElevation = 2.dp, + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surface, + modifier = + Modifier.fillMaxWidth(fillMaxWidth) + .padding(vertical = 10.dp) + .padding(bottom = 140.dp), + ) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + 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) { - TextButton( - enabled = !didExportFiles, onClick = { showAuthPrompt = true }) { - Text(stringResource(R.string.export_configs)) - } + horizontalArrangement = Arrangement.Center, + ) { + TextButton( + enabled = !didExportFiles, + onClick = { showAuthPrompt = true }, + ) { + Text(stringResource(R.string.export_configs)) } - } + } + } } - } - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - Spacer(modifier = Modifier.weight(.17f)) - } + } + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + Spacer(modifier = Modifier.weight(.17f)) + } } - } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsUiState.kt index 21f150e..01f4fe8 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsUiState.kt @@ -5,9 +5,10 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState data class SettingsUiState( - val settings : Settings = Settings(), - val tunnels : List = emptyList(), + val settings: Settings = Settings(), + val tunnels: List = emptyList(), val vpnState: VpnState = VpnState(), - val isLocationDisclosureShown : Boolean = true, - val loading : Boolean = true + val isLocationDisclosureShown: Boolean = true, + val isBatteryOptimizeDisableShown: Boolean = false, + val loading: Boolean = true ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt index 6e91616..5eff576 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt @@ -36,18 +36,29 @@ constructor( private val vpnService: VpnService ) : ViewModel() { - 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()) + val uiState = + combine( + settingsRepository.getSettingsFlow(), + tunnelConfigRepository.getTunnelConfigsFlow(), + vpnService.vpnState, + 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{ + fun onSaveTrustedSSID(ssid: String): Result { val trimmed = ssid.trim() return if (!uiState.value.settings.trustedNetworkSSIDs.contains(trimmed)) { uiState.value.settings.trustedNetworkSSIDs.add(trimmed) @@ -58,64 +69,77 @@ constructor( } } - fun setLocationDisclosureShown() = viewModelScope.launch { + fun setLocationDisclosureShown() = + viewModelScope.launch { dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, true) - } + } + + 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() - )) - } - - private suspend fun getDefaultTunnelOrFirst() : String { - return uiState.value.settings.defaultTunnel ?: tunnelConfigRepository.getAll().first().toString() - } - - fun toggleAutoTunnel() = viewModelScope.launch { - val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled - var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused - - if (isAutoTunnelEnabled) { - ServiceManager.stopWatcherService(application) - } else { - ServiceManager.startWatcherService(application) - isAutoTunnelPaused = false - } saveSettings( uiState.value.settings.copy( - isAutoTunnelEnabled = !isAutoTunnelEnabled, - isAutoTunnelPaused = isAutoTunnelPaused, - defaultTunnel = getDefaultTunnelOrFirst() - ) + trustedNetworkSSIDs = + (uiState.value.settings.trustedNetworkSSIDs - ssid).toMutableList(), + ), ) } + private suspend fun getDefaultTunnelOrFirst(): String { + return uiState.value.settings.defaultTunnel + ?: tunnelConfigRepository.getAll().first().toString() + } - fun onToggleAlwaysOnVPN() = viewModelScope.launch { - val updatedSettings = uiState.value.settings.copy( - isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled, - defaultTunnel = getDefaultTunnelOrFirst() + fun toggleAutoTunnel() = + viewModelScope.launch { + val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled + var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused + + if (isAutoTunnelEnabled) { + ServiceManager.stopWatcherService(application) + } else { + ServiceManager.startWatcherService(application) + isAutoTunnelPaused = false + } + saveSettings( + uiState.value.settings.copy( + isAutoTunnelEnabled = !isAutoTunnelEnabled, + isAutoTunnelPaused = isAutoTunnelPaused, + defaultTunnel = getDefaultTunnelOrFirst(), + ), ) - saveSettings(updatedSettings) - } + } - private fun saveSettings(settings: Settings) = viewModelScope.launch { - settingsRepository.save(settings) - } + fun onToggleAlwaysOnVPN() = + viewModelScope.launch { + val updatedSettings = + uiState.value.settings.copy( + isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled, + defaultTunnel = getDefaultTunnelOrFirst(), + ) + saveSettings(updatedSettings) + } + + private fun saveSettings(settings: Settings) = + viewModelScope.launch { settingsRepository.save(settings) } fun onToggleTunnelOnEthernet() { - 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,36 +150,36 @@ 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, + ), ) } - fun onToggleKernelMode() : Result { + fun onToggleKernelMode(): Result { if (!uiState.value.settings.isKernelEnabled) { try { rootShell.start() diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt index 518211d..8be48c7 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt @@ -62,39 +62,43 @@ fun SupportScreen( showSnackbarMessage: (String) -> Unit, focusRequester: FocusRequester ) { - val context = LocalContext.current - val fillMaxWidth = .85f + val context = LocalContext.current + val fillMaxWidth = .85f - val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() - fun openWebPage(url: String) { - try { - val webpage: Uri = Uri.parse(url) - val intent = Intent(Intent.ACTION_VIEW, webpage) - context.startActivity(intent) - } catch (e : Exception) { - showSnackbarMessage(Event.Error.Exception(e).message) - } - } + fun openWebPage(url: String) { + try { + val webpage: Uri = Uri.parse(url) + val intent = Intent(Intent.ACTION_VIEW, webpage) + context.startActivity(intent) + } catch (e: Exception) { + showSnackbarMessage(Event.Error.Exception(e).message) + } + } - fun launchEmail() { - try { - val intent = - Intent(Intent.ACTION_SEND).apply { - type = Constants.EMAIL_MIME_TYPE - putExtra(Intent.EXTRA_EMAIL, 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) - } catch (e : Exception) { - showSnackbarMessage(Event.Error.Exception(e).message) - } - } + fun launchEmail() { + try { + val intent = + Intent(Intent.ACTION_SENDTO).apply { + type = Constants.EMAIL_MIME_TYPE + 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, + ) + } catch (e: Exception) { + showSnackbarMessage(Event.Error.Exception(e).message) + } + } - if (uiState.loading) { - LoadingScreen() - return - } + if (uiState.loading) { + LoadingScreen() + return + } Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -103,126 +107,147 @@ fun SupportScreen( Modifier.fillMaxSize() .verticalScroll(rememberScrollState()) .focusable() - .padding(padding)) { - Surface( - tonalElevation = 2.dp, - shadowElevation = 2.dp, - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surface, - modifier = - (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + .padding(padding), + ) { + Surface( + tonalElevation = 2.dp, + shadowElevation = 2.dp, + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surface, + modifier = + (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { Modifier.height(IntrinsicSize.Min) .fillMaxWidth(fillMaxWidth) .padding(top = 10.dp) - } else { + } else { Modifier.fillMaxWidth(fillMaxWidth).padding(top = 20.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) - Text( - stringResource(id = R.string.support_help_text), - textAlign = TextAlign.Start, - fontSize = 16.sp, - modifier = Modifier.padding(bottom = 20.dp)) - TextButton( - onClick = { openWebPage(context.resources.getString(R.string.docs_url)) }, - modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester)) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - 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)) - } - Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go)) - } - } - Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp) - TextButton( - onClick = { openWebPage(context.resources.getString(R.string.discord_url)) }, - modifier = Modifier.padding(vertical = 5.dp)) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth()) { - Row { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.discord), - stringResource(id = R.string.discord), - Modifier.size(25.dp)) - Text( - stringResource(id = R.string.discord_description), - textAlign = TextAlign.Justify, - modifier = Modifier.padding(start = 10.dp)) - } - Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go)) - } - } - Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp) - TextButton( - onClick = { openWebPage(context.resources.getString(R.string.github_url)) }, - modifier = Modifier.padding(vertical = 5.dp)) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth()) { - Row { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.github), - stringResource(id = R.string.github), - Modifier.size(25.dp)) - Text( - "Open an issue", - textAlign = TextAlign.Justify, - modifier = Modifier.padding(start = 10.dp)) - } - Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go)) - } - } - Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp) - TextButton( - onClick = { launchEmail() }, modifier = Modifier.padding(vertical = 5.dp)) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth()) { - Row { - Icon(Icons.Rounded.Mail, stringResource(id = R.string.email)) - Text( - stringResource(id = R.string.email_description), - textAlign = TextAlign.Justify, - modifier = Modifier.padding(start = 10.dp)) - } - Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go)) - } - } + }) + .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, + ) + Text( + stringResource(id = R.string.support_help_text), + textAlign = TextAlign.Start, + fontSize = 16.sp, + modifier = Modifier.padding(bottom = 20.dp), + ) + TextButton( + onClick = { openWebPage(context.resources.getString(R.string.docs_url)) }, + modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester), + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + 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), + ) + } + Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go)) + } } - } - Spacer(modifier = Modifier.weight(1f)) - Text( - stringResource(id = R.string.privacy_policy), - style = TextStyle(textDecoration = TextDecoration.Underline), - fontSize = 16.sp, - modifier = - Modifier.clickable { - openWebPage(context.resources.getString(R.string.privacy_policy_url)) - }) - Row( - horizontalArrangement = Arrangement.spacedBy(25.dp), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(25.dp)) { - Text("Version: ${BuildConfig.VERSION_NAME}", modifier = Modifier.focusable()) - Text("Mode: ${if (uiState.settings.isKernelEnabled) "Kernel" else "Userspace" }") - } + 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), + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Row { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.discord), + stringResource(id = R.string.discord), + Modifier.size(25.dp), + ) + Text( + stringResource(id = R.string.discord_description), + textAlign = TextAlign.Justify, + modifier = Modifier.padding(start = 10.dp), + ) + } + Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go)) + } + } + Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp) + TextButton( + onClick = { openWebPage(context.resources.getString(R.string.github_url)) }, + modifier = Modifier.padding(vertical = 5.dp), + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Row { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.github), + stringResource(id = R.string.github), + Modifier.size(25.dp), + ) + Text( + "Open an issue", + textAlign = TextAlign.Justify, + modifier = Modifier.padding(start = 10.dp), + ) + } + Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go)) + } + } + Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp) + TextButton( + onClick = { launchEmail() }, + modifier = Modifier.padding(vertical = 5.dp), + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Row { + Icon(Icons.Rounded.Mail, stringResource(id = R.string.email)) + Text( + stringResource(id = R.string.email_description), + textAlign = TextAlign.Justify, + modifier = Modifier.padding(start = 10.dp), + ) + } + Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go)) + } + } + } } - } + Spacer(modifier = Modifier.weight(1f)) + Text( + stringResource(id = R.string.privacy_policy), + style = TextStyle(textDecoration = TextDecoration.Underline), + fontSize = 16.sp, + modifier = + Modifier.clickable { + openWebPage(context.resources.getString(R.string.privacy_policy_url)) + }, + ) + Row( + horizontalArrangement = Arrangement.spacedBy(25.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(25.dp), + ) { + Text("Version: ${BuildConfig.VERSION_NAME}", modifier = Modifier.focusable()) + Text("Mode: ${if (uiState.settings.isKernelEnabled) "Kernel" else "Userspace"}") + } + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportUiState.kt index ef5dcc8..e874416 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportUiState.kt @@ -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 -) \ No newline at end of file +data class SupportUiState(val settings: Settings = Settings(), val loading: Boolean = true) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportViewModel.kt index f023ad9..dc9e638 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportViewModel.kt @@ -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( - viewModelScope, - SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), - SupportUiState() - ) + val uiState = + settingsRepository + .getSettingsFlow() + .map { SupportUiState(it, false) } + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), + SupportUiState(), + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Theme.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Theme.kt index d4d529b..0c4770c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Theme.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Theme.kt @@ -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, ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/TransparentSystemBars.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/TransparentSystemBars.kt index 62ee01f..9d353ba 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/TransparentSystemBars.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/TransparentSystemBars.kt @@ -14,7 +14,7 @@ fun TransparentSystemBars() { DisposableEffect(systemUiController, useDarkIcons) { systemUiController.setSystemBarsColor( color = Color.Transparent, - darkIcons = useDarkIcons + darkIcons = useDarkIcons, ) onDispose {} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Type.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Type.kt index 9f0e579..9eac359 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Type.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Type.kt @@ -10,13 +10,13 @@ import androidx.compose.ui.unit.sp val Typography = Typography( bodyLarge = - TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ) + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp, + ), /* Other default text styles to override titleLarge = TextStyle( fontFamily = FontFamily.Default, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Event.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Event.kt index a7da4b0..d95a883 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Event.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Event.kt @@ -12,79 +12,100 @@ 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() { + + 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() { + data object ConfigSaved : Message() { override val message: String get() = WireGuardAutoTunnel.instance.getString(R.string.config_changes_saved) } - data object ConfigsExported: Message() { + + data object ConfigsExported : Message() { override val message: String get() = WireGuardAutoTunnel.instance.getString(R.string.exported_configs_message) } - data object TunnelOffAction: Message() { + + data object TunnelOffAction : Message() { override val message: String get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_tunnel) } - data object TunnelOnAction: Message() { + + data object TunnelOnAction : Message() { override val message: String get() = WireGuardAutoTunnel.instance.getString(R.string.turn_on_tunnel) } - data object AutoTunnelOffAction: Message() { + + data object AutoTunnelOffAction : Message() { override val message: String get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_auto) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt index 7eb0586..acc19ee 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt @@ -37,23 +37,23 @@ fun BigDecimal.toThreeDecimalPlaceString(): String { } fun List.update(index: Int, item: T): List = toMutableList().apply { this[index] = item } + fun List.removeAt(index: Int): List = toMutableList().apply { this.removeAt(index) } typealias TunnelConfigs = List + typealias Packages = List fun Statistics.mapPeerStats(): Map { - return this.peers().associateWith { key -> - (this.peer(key)) - } + return this.peers().associateWith { key -> (this.peer(key)) } } -fun PeerStats.latestHandshakeSeconds() : Long? { +fun PeerStats.latestHandshakeSeconds(): Long? { return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis) } -fun PeerStats.handshakeStatus() : HandshakeStatus { - //TODO add never connected status after duration +fun PeerStats.handshakeStatus(): HandshakeStatus { + // TODO add never connected status after duration return this.latestHandshakeSeconds().let { when { it == null -> HandshakeStatus.NOT_STARTED @@ -65,4 +65,3 @@ fun PeerStats.handshakeStatus() : HandshakeStatus { } } } - diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/FileUtils.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/FileUtils.kt index 2651dab..2c895cd 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/FileUtils.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/FileUtils.kt @@ -36,22 +36,20 @@ object FileUtils { val target = File( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), - fileName + fileName, ) return target.outputStream() } return null } - fun saveFilesToZip( - context: Context, - files: List - ) { - val zipOutputStream = createDownloadsFileOutputStream( - context, - "wg-export_${Instant.now().epochSecond}.zip", - ZIP_FILE_MIME_TYPE - ) + fun saveFilesToZip(context: Context, files: List) { + val zipOutputStream = + createDownloadsFileOutputStream( + context, + "wg-export_${Instant.now().epochSecond}.zip", + ZIP_FILE_MIME_TYPE, + ) ZipOutputStream(zipOutputStream).use { zos -> files.forEach { file -> val entry = ZipEntry(file.name) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Result.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Result.kt index c5d5913..b0515cb 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Result.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Result.kt @@ -2,15 +2,15 @@ package com.zaneschepke.wireguardautotunnel.util import timber.log.Timber - sealed class Result { - class Success(val data: T): Result() - class Error(val error : Event.Error): Result() { + class Success(val data: T) : Result() + + class Error(val error: Event.Error) : Result() { init { - when(this.error) { + when (this.error) { is Event.Error.Exception -> Timber.e(this.error.exception) else -> Timber.e(this.error.message) } } } -} \ No newline at end of file +} diff --git a/app/src/main/res/drawable/discord.xml b/app/src/main/res/drawable/discord.xml index df43339..6532915 100644 --- a/app/src/main/res/drawable/discord.xml +++ b/app/src/main/res/drawable/discord.xml @@ -3,8 +3,8 @@ android:height="800dp" android:viewportWidth="256" android:viewportHeight="256"> - + diff --git a/app/src/main/res/drawable/github.xml b/app/src/main/res/drawable/github.xml index 32f8254..1f23a47 100644 --- a/app/src/main/res/drawable/github.xml +++ b/app/src/main/res/drawable/github.xml @@ -3,10 +3,10 @@ android:height="800dp" android:viewportWidth="20" android:viewportHeight="20"> - + diff --git a/app/src/main/res/drawable/shield.xml b/app/src/main/res/drawable/shield.xml index e49a1f2..ac7ce05 100644 --- a/app/src/main/res/drawable/shield.xml +++ b/app/src/main/res/drawable/shield.xml @@ -1,5 +1,10 @@ - - + + diff --git a/app/src/main/res/drawable/vpn_off.xml b/app/src/main/res/drawable/vpn_off.xml index d33949b..08aa68e 100644 --- a/app/src/main/res/drawable/vpn_off.xml +++ b/app/src/main/res/drawable/vpn_off.xml @@ -1,5 +1,10 @@ - - + + diff --git a/app/src/main/res/drawable/vpn_on.xml b/app/src/main/res/drawable/vpn_on.xml index 1339fb3..a35e535 100644 --- a/app/src/main/res/drawable/vpn_on.xml +++ b/app/src/main/res/drawable/vpn_on.xml @@ -1,5 +1,10 @@ - - + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_banner.xml b/app/src/main/res/mipmap-anydpi-v26/ic_banner.xml index cf3108b..3b3505d 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_banner.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_banner.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 036d09b..c9ad5f9 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 036d09b..c9ad5f9 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7a8b6e1..2a9a7c0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -164,4 +164,7 @@ Location Services Not Detected 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? Auto-tunnel Service + Delete tunnel + Are you sure you would like to delete this tunnel? + Yes \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 9414a53..b703e9d 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,5 +1,6 @@ + diff --git a/app/src/main/res/xml/shortcuts.xml b/app/src/main/res/xml/shortcuts.xml index 0fbe1c3..4d47265 100644 --- a/app/src/main/res/xml/shortcuts.xml +++ b/app/src/main/res/xml/shortcuts.xml @@ -1,31 +1,35 @@ + android:shortcutShortLabel="@string/vpn_on"> - + android:targetClass="com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsActivity" + android:targetPackage="com.zaneschepke.wireguardautotunnel"> + + android:shortcutShortLabel="@string/vpn_off"> - + android:targetClass="com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsActivity" + android:targetPackage="com.zaneschepke.wireguardautotunnel"> + diff --git a/buildSrc/src/main/kotlin/BuildHelper.kt b/buildSrc/src/main/kotlin/BuildHelper.kt index 8d413c5..82a5d7e 100644 --- a/buildSrc/src/main/kotlin/BuildHelper.kt +++ b/buildSrc/src/main/kotlin/BuildHelper.kt @@ -26,11 +26,9 @@ object BuildHelper { } fun isReleaseBuild(gradle: Gradle): Boolean { - return ( - gradle.startParameter.taskNames.size > 0 && - gradle.startParameter.taskNames[0].contains( - "Release", - ) - ) + return (gradle.startParameter.taskNames.size > 0 && + gradle.startParameter.taskNames[0].contains( + "Release", + )) } } diff --git a/buildSrc/src/main/kotlin/Constants.kt b/buildSrc/src/main/kotlin/Constants.kt index 398bc78..8e33e4e 100644 --- a/buildSrc/src/main/kotlin/Constants.kt +++ b/buildSrc/src/main/kotlin/Constants.kt @@ -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" diff --git a/fastlane/Fastfile b/fastlane/Fastfile index a2dec45..7ab1810 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -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 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/33300.txt b/fastlane/metadata/android/en-US/changelogs/33300.txt new file mode 100644 index 0000000..9278b40 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/33300.txt @@ -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 \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c21f0b3..990e8f1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" -composeBom="2023.10.01" -firebaseBom= "32.7.0" -compose="1.5.4" -crashlytics= "18.6.0" -analytics="21.5.0" -composeCompiler="1.5.3" +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" zxingAndroidEmbedded = "4.3.0" zxingCore = "3.5.2" @@ -54,13 +53,13 @@ androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomVers androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomVersion" } #compose -androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref="composeBom" } -androidx-compose-ui-test = { module="androidx.compose.ui:ui-test-junit4", version.ref="compose" } -androidx-compose-ui-tooling = { module="androidx.compose.ui:ui-tooling", version.ref="compose" } -androidx-compose-manifest = { module="androidx.compose.ui:ui-test-manifest", version.ref="compose" } -androidx-compose-ui-graphics = { module="androidx.compose.ui:ui-graphics", version.ref="compose" } -androidx-compose-ui-tooling-preview = { module="androidx.compose.ui:ui-tooling-preview", version.ref="compose" } -androidx-compose-ui = { module="androidx.compose.ui:ui", version.ref="compose" } +androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } +androidx-compose-ui-test = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose" } +androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } +androidx-compose-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "compose" } +androidx-compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics", version.ref = "compose" } +androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } +androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } #hilt androidx-room-testing = { module = "androidx.room:room-testing", version.ref = "roomVersion" } @@ -84,14 +83,13 @@ lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-com material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "material-icons-extended" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } - tunnel = { module = "com.wireguard.android:tunnel", version.ref = "tunnel" } #firebase google-firebase-crashlytics-ktx = { module = "com.google.firebase:firebase-crashlytics-ktx", version.ref = "crashlytics" } google-firebase-analytics-ktx = { module = "com.google.firebase:firebase-analytics-ktx", version.ref = "analytics" } firebase-crashlytics-gradle = { module = "com.google.firebase:firebase-crashlytics-gradle", version.ref = "firebase-crashlytics-gradle" } -firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom"} +firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } google-services = { module = "com.google.gms:google-services", version.ref = "google-services" } zxing-core = { module = "com.google.zxing:core", version.ref = "zxingCore" } @@ -101,4 +99,4 @@ zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", ve android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hiltAndroid" } -ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 308d6d9..9daeafa 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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")