diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index fcefbfeb..00000000 --- a/.editorconfig +++ /dev/null @@ -1,97 +0,0 @@ -root = true - -[*] -charset = utf-8 -indent_size = 4 -indent_style = tab -max_line_length = 150 -trim_trailing_whitespace = true -insert_final_newline = true - -[{*.kt,*.kts}] -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_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_catch_on_new_line = false -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_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 -#compose -ktlint_standard_filename = disabled -ktlint_standard_no-wildcard-imports = disabled -ktlint_standard_function-naming = disabled -ktlint_standard_property-naming = disabled -ktlint_standard_package-naming = disabled -ktlint_function_naming_ignore_when_annotated_with = Composable -ktlint_code_style = android_studio -ktlint_standard_import-ordering = disabled -ktlint_standard_package-naming = disabled -ij_kotlin_allow_trailing_comma = true -ij_kotlin_allow_trailing_comma_on_call_site = true \ No newline at end of file diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml index b5752cf1..1cebab46 100644 --- a/.github/workflows/on-pr.yml +++ b/.github/workflows/on-pr.yml @@ -19,5 +19,5 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew - - name: Run ktlint - run: ./gradlew ktlintCheck + - name: Run ktfmt + run: ./gradlew ktfmtCheck diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8f370a81..6fabc0b1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,252 +1,253 @@ plugins { - alias(libs.plugins.android.application) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.hilt.android) - alias(libs.plugins.kotlinxSerialization) - alias(libs.plugins.ksp) - alias(libs.plugins.compose.compiler) - alias(libs.plugins.grgit) + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.hilt.android) + alias(libs.plugins.kotlinxSerialization) + alias(libs.plugins.ksp) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.grgit) } val versionFile = file("$rootDir/versionCode.txt") -val versionCodeIncrement = with(getBuildTaskName().lowercase()) { - when { - this.contains(Constants.NIGHTLY) || this.contains(Constants.PRERELEASE) -> { - if (versionFile.exists()) { - versionFile.readText().trim().toInt() + 1 - } else { - 1 - } - } - else -> 0 - } -} +val versionCodeIncrement = + with(getBuildTaskName().lowercase()) { + when { + this.contains(Constants.NIGHTLY) || this.contains(Constants.PRERELEASE) -> { + if (versionFile.exists()) { + versionFile.readText().trim().toInt() + 1 + } else { + 1 + } + } + else -> 0 + } + } android { - namespace = Constants.APP_ID - compileSdk = Constants.TARGET_SDK + namespace = Constants.APP_ID + compileSdk = Constants.TARGET_SDK - androidResources { - generateLocaleConfig = true - } + androidResources { generateLocaleConfig = true } - // reproducibility - dependenciesInfo { - // Disables dependency metadata when building APKs. - includeInApk = false - // Disables dependency metadata when building Android App Bundles. - includeInBundle = false - } + // reproducibility + dependenciesInfo { + // Disables dependency metadata when building APKs. + includeInApk = false + // Disables dependency metadata when building Android App Bundles. + includeInBundle = false + } - defaultConfig { - applicationId = Constants.APP_ID - minSdk = Constants.MIN_SDK - targetSdk = Constants.TARGET_SDK - versionCode = Constants.VERSION_CODE + versionCodeIncrement - versionName = determineVersionName() + defaultConfig { + applicationId = Constants.APP_ID + minSdk = Constants.MIN_SDK + targetSdk = Constants.TARGET_SDK + versionCode = Constants.VERSION_CODE + versionCodeIncrement + versionName = determineVersionName() - ksp { arg("room.schemaLocation", "$projectDir/schemas") } + ksp { arg("room.schemaLocation", "$projectDir/schemas") } - sourceSets { - getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room - } + sourceSets { + getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room + } - buildConfigField("String[]", "LANGUAGES", "new String[]{ ${languageList().joinToString(separator = ", ") { "\"$it\"" }} }") + buildConfigField( + "String[]", + "LANGUAGES", + "new String[]{ ${languageList().joinToString(separator = ", ") { "\"$it\"" }} }", + ) - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - vectorDrawables { useSupportLibrary = true } - } + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { useSupportLibrary = true } + } - signingConfigs { - create(Constants.RELEASE) { - storeFile = getStoreFile() - storePassword = getSigningProperty(Constants.STORE_PASS_VAR) - keyAlias = getSigningProperty(Constants.KEY_ALIAS_VAR) - keyPassword = getSigningProperty(Constants.KEY_PASS_VAR) - } - } + signingConfigs { + create(Constants.RELEASE) { + storeFile = getStoreFile() + storePassword = getSigningProperty(Constants.STORE_PASS_VAR) + keyAlias = getSigningProperty(Constants.KEY_ALIAS_VAR) + keyPassword = getSigningProperty(Constants.KEY_PASS_VAR) + } + } - buildTypes { - // don't strip - packaging.jniLibs.keepDebugSymbols.addAll( - listOf("libwg-go.so", "libwg-quick.so", "libwg.so"), - ) + buildTypes { + // don't strip + packaging.jniLibs.keepDebugSymbols.addAll( + listOf("libwg-go.so", "libwg-quick.so", "libwg.so") + ) - release { - isDebuggable = false - isMinifyEnabled = true - isShrinkResources = true - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro", - ) - signingConfig = signingConfigs.getByName(Constants.RELEASE) - resValue("string", "provider", "\"${Constants.APP_NAME}.provider\"") - } - debug { - applicationIdSuffix = ".debug" - resValue("string", "app_name", "WG Tunnel - Debug") - isDebuggable = true - resValue("string", "provider", "\"${Constants.APP_NAME}.provider.debug\"") - } + release { + isDebuggable = false + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + signingConfig = signingConfigs.getByName(Constants.RELEASE) + resValue("string", "provider", "\"${Constants.APP_NAME}.provider\"") + } + debug { + applicationIdSuffix = ".debug" + resValue("string", "app_name", "WG Tunnel - Debug") + isDebuggable = true + resValue("string", "provider", "\"${Constants.APP_NAME}.provider.debug\"") + } - create(Constants.PRERELEASE) { - initWith(buildTypes.getByName(Constants.RELEASE)) - applicationIdSuffix = ".prerelease" - resValue("string", "app_name", "WG Tunnel - Pre") - resValue("string", "provider", "\"${Constants.APP_NAME}.provider.pre\"") - } + create(Constants.PRERELEASE) { + initWith(buildTypes.getByName(Constants.RELEASE)) + applicationIdSuffix = ".prerelease" + resValue("string", "app_name", "WG Tunnel - Pre") + resValue("string", "provider", "\"${Constants.APP_NAME}.provider.pre\"") + } - create(Constants.NIGHTLY) { - initWith(buildTypes.getByName(Constants.RELEASE)) - applicationIdSuffix = ".nightly" - resValue("string", "app_name", "WG Tunnel - Nightly") - resValue("string", "provider", "\"${Constants.APP_NAME}.provider.nightly\"") - } + create(Constants.NIGHTLY) { + initWith(buildTypes.getByName(Constants.RELEASE)) + applicationIdSuffix = ".nightly" + resValue("string", "app_name", "WG Tunnel - Nightly") + resValue("string", "provider", "\"${Constants.APP_NAME}.provider.nightly\"") + } - applicationVariants.all { - val variant = this - variant.outputs - .map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl } - .forEach { output -> - val outputFileName = - "${Constants.APP_NAME}-${variant.flavorName}-" + - "${variant.buildType.name}-${variant.versionName}.apk" - output.outputFileName = outputFileName - } - } - } - flavorDimensions.add(Constants.TYPE) - productFlavors { - create("fdroid") { - dimension = Constants.TYPE - proguardFile("fdroid-rules.pro") - } - create("general") { - dimension = Constants.TYPE - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - isCoreLibraryDesugaringEnabled = true - } - kotlinOptions { jvmTarget = Constants.JVM_TARGET } - buildFeatures { - compose = true - buildConfig = true - } - packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } + applicationVariants.all { + val variant = this + variant.outputs + .map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl } + .forEach { output -> + val outputFileName = + "${Constants.APP_NAME}-${variant.flavorName}-" + + "${variant.buildType.name}-${variant.versionName}.apk" + output.outputFileName = outputFileName + } + } + } + flavorDimensions.add(Constants.TYPE) + productFlavors { + create("fdroid") { + dimension = Constants.TYPE + proguardFile("fdroid-rules.pro") + } + create("general") { dimension = Constants.TYPE } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + isCoreLibraryDesugaringEnabled = true + } + kotlinOptions { jvmTarget = Constants.JVM_TARGET } + buildFeatures { + compose = true + buildConfig = true + } + packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } } dependencies { - implementation(project(":logcatter")) - implementation(project(":networkmonitor")) + implementation(project(":logcatter")) + implementation(project(":networkmonitor")) - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) - // helpers for implementing LifecycleOwner in a Service - implementation(libs.androidx.lifecycle.service) - implementation(libs.androidx.activity.compose) - implementation(platform(libs.androidx.compose.bom)) - implementation(libs.androidx.compose.ui) - implementation(libs.androidx.compose.ui.graphics) - implementation(libs.androidx.compose.ui.tooling.preview) - implementation(libs.androidx.material3) - implementation(libs.androidx.appcompat) - implementation(libs.material) - implementation(libs.androidx.storage) + // helpers for implementing LifecycleOwner in a Service + implementation(libs.androidx.lifecycle.service) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.material3) + implementation(libs.androidx.appcompat) + implementation(libs.material) + implementation(libs.androidx.storage) - // test - testImplementation(libs.junit) - testImplementation(libs.androidx.junit) - androidTestImplementation(libs.androidx.junit) - androidTestImplementation(libs.androidx.espresso.core) - androidTestImplementation(platform(libs.androidx.compose.bom)) - androidTestImplementation(libs.androidx.compose.ui.test) - androidTestImplementation(libs.androidx.room.testing) - debugImplementation(libs.androidx.compose.ui.tooling) - debugImplementation(libs.androidx.compose.manifest) + // test + testImplementation(libs.junit) + testImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.compose.ui.test) + androidTestImplementation(libs.androidx.room.testing) + debugImplementation(libs.androidx.compose.ui.tooling) + debugImplementation(libs.androidx.compose.manifest) - // tunnel - implementation(libs.tunnel) - implementation(libs.amneziawg.android) - coreLibraryDesugaring(libs.desugar.jdk.libs) + // tunnel + implementation(libs.tunnel) + implementation(libs.amneziawg.android) + coreLibraryDesugaring(libs.desugar.jdk.libs) - // logging - implementation(libs.timber) + // logging + implementation(libs.timber) - // compose navigation - implementation(libs.androidx.navigation.compose) - implementation(libs.androidx.hilt.navigation.compose) + // compose navigation + implementation(libs.androidx.navigation.compose) + implementation(libs.androidx.hilt.navigation.compose) - // hilt - implementation(libs.hilt.android) - ksp(libs.hilt.android.compiler) - ksp(libs.androidx.hilt.compiler) + // hilt + implementation(libs.hilt.android) + ksp(libs.hilt.android.compiler) + ksp(libs.androidx.hilt.compiler) - // accompanist - implementation(libs.accompanist.permissions) - implementation(libs.accompanist.drawablepainter) + // accompanist + implementation(libs.accompanist.permissions) + implementation(libs.accompanist.drawablepainter) - // storage - implementation(libs.androidx.room.runtime) - ksp(libs.androidx.room.compiler) - implementation(libs.androidx.room.ktx) - implementation(libs.androidx.datastore.preferences) + // storage + implementation(libs.androidx.room.runtime) + ksp(libs.androidx.room.compiler) + implementation(libs.androidx.room.ktx) + implementation(libs.androidx.datastore.preferences) - // lifecycle - implementation(libs.lifecycle.runtime.compose) - implementation(libs.androidx.lifecycle.runtime.ktx) - implementation(libs.androidx.lifecycle.process) + // lifecycle + implementation(libs.lifecycle.runtime.compose) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.process) - // serialization - implementation(libs.kotlinx.serialization.json) + // serialization + implementation(libs.kotlinx.serialization.json) - // ui - implementation(libs.zxing.android.embedded) - implementation(libs.material.icons.extended) + // ui + implementation(libs.zxing.android.embedded) + implementation(libs.material.icons.extended) - // bio - implementation(libs.androidx.biometric.ktx) - implementation(libs.pin.lock.compose) + // bio + implementation(libs.androidx.biometric.ktx) + implementation(libs.pin.lock.compose) - // shortcuts - implementation(libs.androidx.core) + // shortcuts + implementation(libs.androidx.core) - // splash - implementation(libs.androidx.core.splashscreen) + // splash + implementation(libs.androidx.core.splashscreen) - // worker - implementation(libs.androidx.work.runtime) - implementation(libs.androidx.hilt.work) + // worker + implementation(libs.androidx.work.runtime) + implementation(libs.androidx.hilt.work) } fun determineVersionName(): String { - return with(getBuildTaskName().lowercase()) { - when { - contains(Constants.NIGHTLY) || contains(Constants.PRERELEASE) -> - Constants.VERSION_NAME + - "-${grgitService.service.get().grgit.head().abbreviatedId}" - else -> Constants.VERSION_NAME - } - } + return with(getBuildTaskName().lowercase()) { + when { + contains(Constants.NIGHTLY) || contains(Constants.PRERELEASE) -> + Constants.VERSION_NAME + "-${grgitService.service.get().grgit.head().abbreviatedId}" + else -> Constants.VERSION_NAME + } + } } -val incrementVersionCode by tasks.registering { - doLast { - val versionFile = file("$rootDir/versionCode.txt") - if (versionFile.exists()) { - versionFile.writeText(versionCodeIncrement.toString()) - println("Incremented versionCode to $versionCodeIncrement") - } - } -} +val incrementVersionCode by + tasks.registering { + doLast { + val versionFile = file("$rootDir/versionCode.txt") + if (versionFile.exists()) { + versionFile.writeText(versionCodeIncrement.toString()) + println("Incremented versionCode to $versionCodeIncrement") + } + } + } tasks.whenTaskAdded { - if (name.startsWith("assemble") && !name.lowercase().contains("debug")) { - dependsOn(incrementVersionCode) - } + if (name.startsWith("assemble") && !name.lowercase().contains("debug")) { + dependsOn(incrementVersionCode) + } } diff --git a/app/fdroid-rules.pro b/app/fdroid-rules.pro index 87f74ae2..25468bcb 100644 --- a/app/fdroid-rules.pro +++ b/app/fdroid-rules.pro @@ -39,4 +39,4 @@ -dontwarn org.joda.time.Instant -dontwarn org.slf4j.impl.StaticLoggerBinder -dontwarn org.slf4j.impl.StaticMDCBinder --dontwarn org.slf4j.impl.StaticMarkerBinder \ No newline at end of file +-dontwarn org.slf4j.impl.StaticMarkerBinder diff --git a/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/ExampleInstrumentedTest.kt index e7789473..340f9041 100644 --- a/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/ExampleInstrumentedTest.kt @@ -13,10 +13,10 @@ import org.junit.runner.RunWith */ @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.zaneschepke.wireguardautotunnel", appContext.packageName) - } + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.zaneschepke.wireguardautotunnel", appContext.packageName) + } } diff --git a/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/MigrationTest.kt b/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/MigrationTest.kt index ddad9306..4c73b59d 100644 --- a/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/MigrationTest.kt +++ b/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/MigrationTest.kt @@ -5,40 +5,35 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.zaneschepke.wireguardautotunnel.data.AppDatabase import com.zaneschepke.wireguardautotunnel.data.Queries +import java.io.IOException import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import java.io.IOException @RunWith(AndroidJUnit4::class) class MigrationTest { - private val dbName = "migration-test" + private val dbName = "migration-test" - @get:Rule - val helper: MigrationTestHelper = - MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java, - ) + @get:Rule + val helper: MigrationTestHelper = + MigrationTestHelper(InstrumentationRegistry.getInstrumentation(), AppDatabase::class.java) - @Test - @Throws(IOException::class) - fun migrate6To7() { - helper.createDatabase(dbName, 6).apply { - // Database has schema version 1. Insert some data using SQL queries. - // You can't use DAO classes because they expect the latest schema. - execSQL(Queries.createDefaultSettings()) - execSQL( - Queries.createTunnelConfig(), - ) - // Prepare for the next version. - close() - } + @Test + @Throws(IOException::class) + fun migrate6To7() { + helper.createDatabase(dbName, 6).apply { + // Database has schema version 1. Insert some data using SQL queries. + // You can't use DAO classes because they expect the latest schema. + execSQL(Queries.createDefaultSettings()) + execSQL(Queries.createTunnelConfig()) + // Prepare for the next version. + close() + } - // Re-open the database with version 2 and provide - // MIGRATION_1_2 as the migration process. - helper.runMigrationsAndValidate(dbName, 7, true) - // MigrationTestHelper automatically verifies the schema changes, - // but you need to validate that the data was migrated properly. - } + // Re-open the database with version 2 and provide + // MIGRATION_1_2 as the migration process. + helper.runMigrationsAndValidate(dbName, 7, true) + // MigrationTestHelper automatically verifies the schema changes, + // but you need to validate that the data was migrated properly. + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt index c2683a0b..4ca51737 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt @@ -10,7 +10,6 @@ import android.os.Build import android.os.Bundle import android.provider.Settings import androidx.activity.SystemBarStyle -import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge @@ -69,6 +68,8 @@ import com.zaneschepke.wireguardautotunnel.ui.common.navigation.DynamicTopAppBar import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.common.navigation.currentNavBackStackEntryAsNavBarState import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar +import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.AutoTunnelScreen +import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced.AutoTunnelAdvancedScreen import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.TunnelAutoTunnelScreen import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.ConfigScreen @@ -77,12 +78,10 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.SplitTunn import com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.TunnelOptionsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.pin.PinLockScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen +import com.zaneschepke.wireguardautotunnel.ui.screens.settings.advanced.SettingsAdvancedScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.AppearanceScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display.DisplayScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen -import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.AutoTunnelScreen -import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced.AutoTunnelAdvancedScreen -import com.zaneschepke.wireguardautotunnel.ui.screens.settings.advanced.SettingsAdvancedScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.disclosure.LocationDisclosureScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.KillSwitchScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen @@ -92,296 +91,304 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.goFromRoot import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent import dagger.hilt.android.AndroidEntryPoint -import org.amnezia.awg.backend.GoBackend.VpnService -import timber.log.Timber import javax.inject.Inject import kotlin.system.exitProcess +import org.amnezia.awg.backend.GoBackend.VpnService +import timber.log.Timber @AndroidEntryPoint class MainActivity : AppCompatActivity() { - @Inject - lateinit var appStateRepository: AppStateRepository + @Inject lateinit var appStateRepository: AppStateRepository - @Inject - lateinit var tunnelManager: TunnelManager + @Inject lateinit var tunnelManager: TunnelManager - @Inject - lateinit var networkMonitor: NetworkMonitor + @Inject lateinit var networkMonitor: NetworkMonitor - private var lastLocationPermissionState: Boolean? = null + private var lastLocationPermissionState: Boolean? = null - @SuppressLint("BatteryLife") - override fun onCreate(savedInstanceState: Bundle?) { - enableEdgeToEdge( - statusBarStyle = SystemBarStyle.Companion.auto(Color.TRANSPARENT, Color.TRANSPARENT), - navigationBarStyle = SystemBarStyle.Companion.auto(Color.TRANSPARENT, Color.TRANSPARENT), - ) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - window.isNavigationBarContrastEnforced = false - } - super.onCreate(savedInstanceState) + @SuppressLint("BatteryLife") + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge( + statusBarStyle = SystemBarStyle.Companion.auto(Color.TRANSPARENT, Color.TRANSPARENT), + navigationBarStyle = SystemBarStyle.Companion.auto(Color.TRANSPARENT, Color.TRANSPARENT), + ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + window.isNavigationBarContrastEnforced = false + } + super.onCreate(savedInstanceState) - val viewModel by viewModels() + val viewModel by viewModels() - installSplashScreen().apply { - setKeepOnScreenCondition { - !viewModel.appViewState.value.isAppReady - } - } + installSplashScreen().apply { + setKeepOnScreenCondition { !viewModel.appViewState.value.isAppReady } + } - setContent { - val appUiState by viewModel.uiState.collectAsStateWithLifecycle() - val appViewState by viewModel.appViewState.collectAsStateWithLifecycle() + setContent { + val appUiState by viewModel.uiState.collectAsStateWithLifecycle() + val appViewState by viewModel.appViewState.collectAsStateWithLifecycle() - val navController = rememberNavController() - val backStackEntry by navController.currentBackStackEntryAsState() - val navBarState by currentNavBackStackEntryAsNavBarState(navController, backStackEntry, viewModel, appUiState) - val snackbar = remember { SnackbarHostState() } - var showVpnPermissionDialog by remember { mutableStateOf(false) } - var vpnPermissionDenied by remember { mutableStateOf(false) } + val navController = rememberNavController() + val backStackEntry by navController.currentBackStackEntryAsState() + val navBarState by + currentNavBackStackEntryAsNavBarState( + navController, + backStackEntry, + viewModel, + appUiState, + ) + val snackbar = remember { SnackbarHostState() } + var showVpnPermissionDialog by remember { mutableStateOf(false) } + var vpnPermissionDenied by remember { mutableStateOf(false) } - val vpnActivity = - rememberLauncherForActivityResult( - ActivityResultContracts.StartActivityForResult(), - onResult = { - if (it.resultCode != RESULT_OK) { - showVpnPermissionDialog = true - vpnPermissionDenied = true - } else { - vpnPermissionDenied = false - } - }, - ) + val vpnActivity = + rememberLauncherForActivityResult( + ActivityResultContracts.StartActivityForResult(), + onResult = { + if (it.resultCode != RESULT_OK) { + showVpnPermissionDialog = true + vpnPermissionDenied = true + } else { + vpnPermissionDenied = false + } + }, + ) - LaunchedEffect(appUiState.tunnels) { - if (!appViewState.isAppReady) { - viewModel.handleEvent(AppEvent.AppReadyCheck(appUiState.tunnels)) - } - } + LaunchedEffect(appUiState.tunnels) { + if (!appViewState.isAppReady) { + viewModel.handleEvent(AppEvent.AppReadyCheck(appUiState.tunnels)) + } + } - val batteryActivity = rememberLauncherForActivityResult( - ActivityResultContracts.StartActivityForResult(), - ) { _: ActivityResult -> - viewModel.handleEvent(AppEvent.SetBatteryOptimizeDisableShown) - } + val batteryActivity = + rememberLauncherForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { _: ActivityResult -> + viewModel.handleEvent(AppEvent.SetBatteryOptimizeDisableShown) + } - with(appViewState) { - LaunchedEffect(isConfigChanged) { - if (isConfigChanged) { - Intent(this@MainActivity, MainActivity::class.java).also { - startActivity(it) - exitProcess(0) - } - } - } - LaunchedEffect(errorMessage) { - errorMessage?.let { - snackbar.showSnackbar(it.asString(this@MainActivity)) - viewModel.handleEvent(AppEvent.MessageShown) - } - } - LaunchedEffect(appUiState.activeTunnels) { - appUiState.activeTunnels - .mapNotNull { (tunnelConf, tunnelState) -> - (tunnelState.status as? TunnelStatus.Error)?.let { error -> - val message = error.error.toStringRes() - val context = this@MainActivity - snackbar.showSnackbar(context.getString(R.string.tunnel_error_template, context.getString(message))) - viewModel.handleEvent(AppEvent.ClearTunnelError(tunnelConf)) - } - } - } - LaunchedEffect(popBackStack) { - if (popBackStack) { - navController.popBackStack() - viewModel.handleEvent(AppEvent.PopBackStack(false)) - } - } - LaunchedEffect(requestVpnPermission) { - if (requestVpnPermission) { - if (!vpnPermissionDenied) { - vpnActivity.launch(VpnService.prepare(this@MainActivity)) - } else { - showVpnPermissionDialog = true - } - viewModel.handleEvent(AppEvent.VpnPermissionRequested) - } - } - LaunchedEffect(requestBatteryPermission) { - if (requestBatteryPermission) { - batteryActivity.launch( - Intent().apply { - action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS - data = Uri.parse("package:${this@MainActivity.packageName}") - }, - ) - } - } - } + with(appViewState) { + LaunchedEffect(isConfigChanged) { + if (isConfigChanged) { + Intent(this@MainActivity, MainActivity::class.java).also { + startActivity(it) + exitProcess(0) + } + } + } + LaunchedEffect(errorMessage) { + errorMessage?.let { + snackbar.showSnackbar(it.asString(this@MainActivity)) + viewModel.handleEvent(AppEvent.MessageShown) + } + } + LaunchedEffect(appUiState.activeTunnels) { + appUiState.activeTunnels.mapNotNull { (tunnelConf, tunnelState) -> + (tunnelState.status as? TunnelStatus.Error)?.let { error -> + val message = error.error.toStringRes() + val context = this@MainActivity + snackbar.showSnackbar( + context.getString( + R.string.tunnel_error_template, + context.getString(message), + ) + ) + viewModel.handleEvent(AppEvent.ClearTunnelError(tunnelConf)) + } + } + } + LaunchedEffect(popBackStack) { + if (popBackStack) { + navController.popBackStack() + viewModel.handleEvent(AppEvent.PopBackStack(false)) + } + } + LaunchedEffect(requestVpnPermission) { + if (requestVpnPermission) { + if (!vpnPermissionDenied) { + vpnActivity.launch(VpnService.prepare(this@MainActivity)) + } else { + showVpnPermissionDialog = true + } + viewModel.handleEvent(AppEvent.VpnPermissionRequested) + } + } + LaunchedEffect(requestBatteryPermission) { + if (requestBatteryPermission) { + batteryActivity.launch( + Intent().apply { + action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS + data = Uri.parse("package:${this@MainActivity.packageName}") + } + ) + } + } + } - CompositionLocalProvider(LocalNavController provides navController) { - WireguardAutoTunnelTheme(theme = appUiState.appState.theme) { - VpnDeniedDialog(showVpnPermissionDialog, onDismiss = { showVpnPermissionDialog = false }) + CompositionLocalProvider(LocalNavController provides navController) { + WireguardAutoTunnelTheme(theme = appUiState.appState.theme) { + VpnDeniedDialog( + showVpnPermissionDialog, + onDismiss = { showVpnPermissionDialog = false }, + ) - Scaffold( - modifier = Modifier.pointerInput(Unit) { - detectTapGestures { - viewModel.handleEvent(AppEvent.SetSelectedTunnel(null)) - } - }, - snackbarHost = { - SnackbarHost(snackbar) { snackbarData: SnackbarData -> - CustomSnackBar( - snackbarData.visuals.message, - isRtl = false, - containerColor = - MaterialTheme.colorScheme.surfaceColorAtElevation( - 2.dp, - ), - ) - } - }, - topBar = { - DynamicTopAppBar(navBarState) - }, - bottomBar = { - AnimatedVisibility( - visible = navBarState.showBottom, - enter = slideInVertically(initialOffsetY = { it }), - exit = slideOutVertically(targetOffsetY = { it }), - ) { - CustomBottomNavbar( - listOf( - BottomNavItem( - name = stringResource(R.string.tunnels), - route = Route.Main, - icon = Icons.Rounded.Home, - onClick = { navController.goFromRoot(Route.Main) }, - ), - BottomNavItem( - name = stringResource(R.string.auto_tunnel), - route = Route.AutoTunnel, - icon = Icons.Rounded.Bolt, - onClick = { - val route = if (appUiState.appState.isLocationDisclosureShown) Route.AutoTunnel else Route.LocationDisclosure - navController.goFromRoot(route) - }, - active = appUiState.isAutoTunnelActive, - ), - BottomNavItem( - name = stringResource(R.string.settings), - route = Route.Settings, - icon = Icons.Rounded.Settings, - onClick = { navController.goFromRoot(Route.Settings) }, - ), - BottomNavItem( - name = stringResource(R.string.support), - route = Route.Support, - icon = Icons.Rounded.QuestionMark, - onClick = { navController.goFromRoot(Route.Support) }, - ), - ), - navBarState = navBarState, - ) - } - }, - ) { padding -> - Box( - modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.surface) - .padding(padding) - .consumeWindowInsets(padding) - .imePadding(), - ) { - NavHost( - navController, - startDestination = (if (appUiState.appState.isPinLockEnabled) Route.Lock else Route.Main), - ) { - composable { - MainScreen(appUiState, appViewState, viewModel) - } - composable { - SettingsScreen(appUiState, appViewState, viewModel) - } - composable { - SettingsAdvancedScreen(appUiState, viewModel) - } - composable { - LocationDisclosureScreen(appUiState, viewModel) - } - composable { - AutoTunnelScreen(appUiState, viewModel) - } - composable { - AppearanceScreen() - } - composable { - LanguageScreen(appUiState, viewModel) - } - composable { - DisplayScreen(appUiState, viewModel) - } - composable { - SupportScreen() - } - composable { - AutoTunnelAdvancedScreen(appUiState, viewModel) - } - composable { - LogsScreen(appViewState, viewModel) - } - composable { backStack -> - val args = backStack.toRoute() - val config = appUiState.tunnels.firstOrNull { it.id == args.id } - ConfigScreen(config, viewModel) - } - composable { backStack -> - val args = backStack.toRoute() - appUiState.tunnels.firstOrNull { it.id == args.id }?.let { config -> - TunnelOptionsScreen(config, appUiState, viewModel) - } - } - composable { - PinLockScreen(viewModel) - } - composable { - ScannerScreen(viewModel) - } - composable { - KillSwitchScreen(appUiState, viewModel) - } - composable { - SplitTunnelScreen(viewModel) - } - composable { backStack -> - val args = backStack.toRoute() - appUiState.tunnels.firstOrNull { it.id == args.id }?.let { - TunnelAutoTunnelScreen(it, appUiState.appSettings, viewModel) - } - } - } - } - } - } - } - } - } - override fun onResume() { - super.onResume() - checkPermissionAndNotify() - } + Scaffold( + modifier = + Modifier.pointerInput(Unit) { + detectTapGestures { + viewModel.handleEvent(AppEvent.SetSelectedTunnel(null)) + } + }, + snackbarHost = { + SnackbarHost(snackbar) { snackbarData: SnackbarData -> + CustomSnackBar( + snackbarData.visuals.message, + isRtl = false, + containerColor = + MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp), + ) + } + }, + topBar = { DynamicTopAppBar(navBarState) }, + bottomBar = { + AnimatedVisibility( + visible = navBarState.showBottom, + enter = slideInVertically(initialOffsetY = { it }), + exit = slideOutVertically(targetOffsetY = { it }), + ) { + CustomBottomNavbar( + listOf( + BottomNavItem( + name = stringResource(R.string.tunnels), + route = Route.Main, + icon = Icons.Rounded.Home, + onClick = { navController.goFromRoot(Route.Main) }, + ), + BottomNavItem( + name = stringResource(R.string.auto_tunnel), + route = Route.AutoTunnel, + icon = Icons.Rounded.Bolt, + onClick = { + val route = + if ( + appUiState.appState + .isLocationDisclosureShown + ) + Route.AutoTunnel + else Route.LocationDisclosure + navController.goFromRoot(route) + }, + active = appUiState.isAutoTunnelActive, + ), + BottomNavItem( + name = stringResource(R.string.settings), + route = Route.Settings, + icon = Icons.Rounded.Settings, + onClick = { navController.goFromRoot(Route.Settings) }, + ), + BottomNavItem( + name = stringResource(R.string.support), + route = Route.Support, + icon = Icons.Rounded.QuestionMark, + onClick = { navController.goFromRoot(Route.Support) }, + ), + ), + navBarState = navBarState, + ) + } + }, + ) { padding -> + Box( + modifier = + Modifier.fillMaxSize() + .background(MaterialTheme.colorScheme.surface) + .padding(padding) + .consumeWindowInsets(padding) + .imePadding() + ) { + NavHost( + navController, + startDestination = + (if (appUiState.appState.isPinLockEnabled) Route.Lock + else Route.Main), + ) { + composable { + MainScreen(appUiState, appViewState, viewModel) + } + composable { + SettingsScreen(appUiState, appViewState, viewModel) + } + composable { + SettingsAdvancedScreen(appUiState, viewModel) + } + composable { + LocationDisclosureScreen(appUiState, viewModel) + } + composable { + AutoTunnelScreen(appUiState, viewModel) + } + composable { AppearanceScreen() } + composable { LanguageScreen(appUiState, viewModel) } + composable { DisplayScreen(appUiState, viewModel) } + composable { SupportScreen() } + composable { + AutoTunnelAdvancedScreen(appUiState, viewModel) + } + composable { LogsScreen(appViewState, viewModel) } + composable { backStack -> + val args = backStack.toRoute() + val config = appUiState.tunnels.firstOrNull { it.id == args.id } + ConfigScreen(config, viewModel) + } + composable { backStack -> + val args = backStack.toRoute() + appUiState.tunnels + .firstOrNull { it.id == args.id } + ?.let { config -> + TunnelOptionsScreen(config, appUiState, viewModel) + } + } + composable { PinLockScreen(viewModel) } + composable { ScannerScreen(viewModel) } + composable { + KillSwitchScreen(appUiState, viewModel) + } + composable { SplitTunnelScreen(viewModel) } + composable { backStack -> + val args = backStack.toRoute() + appUiState.tunnels + .firstOrNull { it.id == args.id } + ?.let { + TunnelAutoTunnelScreen( + it, + appUiState.appSettings, + viewModel, + ) + } + } + } + } + } + } + } + } + } - private fun checkPermissionAndNotify() { - val hasLocation = ContextCompat.checkSelfPermission( - this, - Manifest.permission.ACCESS_FINE_LOCATION, - ) == PackageManager.PERMISSION_GRANTED - if (lastLocationPermissionState != hasLocation) { - Timber.d("Location permission changed to: $hasLocation") - if (hasLocation) { - networkMonitor.sendLocationPermissionsGrantedBroadcast() - } - lastLocationPermissionState = hasLocation - } - } + override fun onResume() { + super.onResume() + checkPermissionAndNotify() + } + + private fun checkPermissionAndNotify() { + val hasLocation = + ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == + PackageManager.PERMISSION_GRANTED + if (lastLocationPermissionState != hasLocation) { + Timber.d("Location permission changed to: $hasLocation") + if (hasLocation) { + networkMonitor.sendLocationPermissionsGrantedBroadcast() + } + lastLocationPermissionState = hasLocation + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt index 0411a57c..48c971c5 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt @@ -20,134 +20,118 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.util.LocaleUtil import com.zaneschepke.wireguardautotunnel.util.ReleaseTree import dagger.hilt.android.HiltAndroidApp +import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber -import javax.inject.Inject @HiltAndroidApp class WireGuardAutoTunnel : Application(), Configuration.Provider { - @Inject - lateinit var workerFactory: HiltWorkerFactory + @Inject lateinit var workerFactory: HiltWorkerFactory - override val workManagerConfiguration: Configuration - get() = Configuration.Builder() - .setWorkerFactory(workerFactory) - .build() + override val workManagerConfiguration: Configuration + get() = Configuration.Builder().setWorkerFactory(workerFactory).build() - @Inject - @ApplicationScope - lateinit var applicationScope: CoroutineScope + @Inject @ApplicationScope lateinit var applicationScope: CoroutineScope - @Inject - lateinit var logReader: LogReader + @Inject lateinit var logReader: LogReader - @Inject - lateinit var appDataRepository: AppDataRepository + @Inject lateinit var appDataRepository: AppDataRepository - @Inject - @IoDispatcher - lateinit var ioDispatcher: CoroutineDispatcher + @Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher - @Inject - @MainDispatcher - lateinit var mainDispatcher: CoroutineDispatcher + @Inject @MainDispatcher lateinit var mainDispatcher: CoroutineDispatcher - @Inject - lateinit var tunnelManager: TunnelManager + @Inject lateinit var tunnelManager: TunnelManager - override fun onCreate() { - super.onCreate() - instance = this - ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver()) - if (BuildConfig.DEBUG) { - Timber.plant(Timber.DebugTree()) - StrictMode.setThreadPolicy( - ThreadPolicy.Builder() - .detectDiskReads() - .detectDiskWrites() - .detectNetwork() - .penaltyLog() - .build(), - ) - } else { - Timber.plant(ReleaseTree()) - } + override fun onCreate() { + super.onCreate() + instance = this + ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver()) + if (BuildConfig.DEBUG) { + Timber.plant(Timber.DebugTree()) + StrictMode.setThreadPolicy( + ThreadPolicy.Builder() + .detectDiskReads() + .detectDiskWrites() + .detectNetwork() + .penaltyLog() + .build() + ) + } else { + Timber.plant(ReleaseTree()) + } - GoBackend.setAlwaysOnCallback { - applicationScope.launch { - val settings = appDataRepository.settings.get() - if (settings.isAlwaysOnVpnEnabled) { - val tunnel = appDataRepository.getPrimaryOrFirstTunnel() - tunnel?.let { - tunnelManager.startTunnel(it) - } - } else { - Timber.w("Always-on VPN is not enabled in app settings") - } - } - } + GoBackend.setAlwaysOnCallback { + applicationScope.launch { + val settings = appDataRepository.settings.get() + if (settings.isAlwaysOnVpnEnabled) { + val tunnel = appDataRepository.getPrimaryOrFirstTunnel() + tunnel?.let { tunnelManager.startTunnel(it) } + } else { + Timber.w("Always-on VPN is not enabled in app settings") + } + } + } - ServiceWorker.start(this) + ServiceWorker.start(this) - applicationScope.launch { - if (!appDataRepository.settings.get().isKernelEnabled) { - tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList()) - } - appDataRepository.appState.getLocale()?.let { - withContext(mainDispatcher) { - LocaleUtil.changeLocale(it) - } - } - appDataRepository.appState.isLocalLogsEnabled().let { enabled -> - if (enabled) logReader.start() - } - } - } + applicationScope.launch { + if (!appDataRepository.settings.get().isKernelEnabled) { + tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList()) + } + appDataRepository.appState.getLocale()?.let { + withContext(mainDispatcher) { LocaleUtil.changeLocale(it) } + } + appDataRepository.appState.isLocalLogsEnabled().let { enabled -> + if (enabled) logReader.start() + } + } + } - override fun onTerminate() { - applicationScope.launch { - tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList()) - } - super.onTerminate() - } + override fun onTerminate() { + applicationScope.launch { + tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList()) + } + super.onTerminate() + } - class AppLifecycleObserver : DefaultLifecycleObserver { + class AppLifecycleObserver : DefaultLifecycleObserver { - override fun onStart(owner: LifecycleOwner) { - Timber.d("Application entered foreground") - foreground = true - } - override fun onPause(owner: LifecycleOwner) { - Timber.d("Application entered background") - foreground = false - } - } + override fun onStart(owner: LifecycleOwner) { + Timber.d("Application entered foreground") + foreground = true + } - companion object { - private var foreground = false + override fun onPause(owner: LifecycleOwner) { + Timber.d("Application entered background") + foreground = false + } + } - fun isForeground(): Boolean { - return foreground - } + companion object { + private var foreground = false - @Volatile - private var lastActiveTunnels: List = emptyList() + fun isForeground(): Boolean { + return foreground + } - @Synchronized - fun getLastActiveTunnels(): List { - return lastActiveTunnels - } + @Volatile private var lastActiveTunnels: List = emptyList() - @Synchronized - fun setLastActiveTunnels(newTunnels: List) { - lastActiveTunnels = newTunnels - } + @Synchronized + fun getLastActiveTunnels(): List { + return lastActiveTunnels + } - lateinit var instance: WireGuardAutoTunnel - private set - } + @Synchronized + fun setLastActiveTunnels(newTunnels: List) { + lastActiveTunnels = newTunnels + } + + lateinit var instance: WireGuardAutoTunnel + private set + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/broadcast/KernelReceiver.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/broadcast/KernelReceiver.kt index f0902d5d..96e6cc5c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/broadcast/KernelReceiver.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/broadcast/KernelReceiver.kt @@ -8,42 +8,35 @@ import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import javax.inject.Inject @AndroidEntryPoint class KernelReceiver : BroadcastReceiver() { - @Inject - @ApplicationScope - lateinit var applicationScope: CoroutineScope + @Inject @ApplicationScope lateinit var applicationScope: CoroutineScope - @Inject - lateinit var tunnelRepository: TunnelRepository + @Inject lateinit var tunnelRepository: TunnelRepository - @Inject - lateinit var serviceManager: ServiceManager + @Inject lateinit var serviceManager: ServiceManager - @Inject - lateinit var tunnelManager: TunnelManager + @Inject lateinit var tunnelManager: TunnelManager - override fun onReceive(context: Context, intent: Intent) { - val action = intent.action ?: return - applicationScope.launch { - if (action == REFRESH_TUNNELS_ACTION) { - tunnelManager.runningTunnelNames().forEach { name -> - val tunnel = tunnelRepository.findByTunnelName(name) - tunnel?.let { - tunnelRepository.save(it.copy(isActive = true)) - } - } - serviceManager.updateTunnelTile() - } - } - } + override fun onReceive(context: Context, intent: Intent) { + val action = intent.action ?: return + applicationScope.launch { + if (action == REFRESH_TUNNELS_ACTION) { + tunnelManager.runningTunnelNames().forEach { name -> + val tunnel = tunnelRepository.findByTunnelName(name) + tunnel?.let { tunnelRepository.save(it.copy(isActive = true)) } + } + serviceManager.updateTunnelTile() + } + } + } - companion object { - const val REFRESH_TUNNELS_ACTION = "com.wireguard.android.action.REFRESH_TUNNEL_STATES" - } + companion object { + const val REFRESH_TUNNELS_ACTION = "com.wireguard.android.action.REFRESH_TUNNEL_STATES" + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/broadcast/NotificationActionReceiver.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/broadcast/NotificationActionReceiver.kt index 4a486f6d..6cfb0673 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/broadcast/NotificationActionReceiver.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/broadcast/NotificationActionReceiver.kt @@ -10,41 +10,36 @@ import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import javax.inject.Inject @AndroidEntryPoint class NotificationActionReceiver : BroadcastReceiver() { - @Inject - lateinit var serviceManager: ServiceManager + @Inject lateinit var serviceManager: ServiceManager - @Inject - lateinit var tunnelManager: TunnelManager + @Inject lateinit var tunnelManager: TunnelManager - @Inject - lateinit var tunnelRepository: TunnelRepository + @Inject lateinit var tunnelRepository: TunnelRepository - @Inject - @ApplicationScope - lateinit var applicationScope: CoroutineScope + @Inject @ApplicationScope lateinit var applicationScope: CoroutineScope - override fun onReceive(context: Context, intent: Intent) { - applicationScope.launch { - when (intent.action) { - NotificationAction.AUTO_TUNNEL_OFF.name -> serviceManager.stopAutoTunnel() - NotificationAction.TUNNEL_OFF.name -> { - val tunnelId = intent.getIntExtra(NotificationManager.EXTRA_ID, 0) - if (tunnelId == STOP_ALL_TUNNELS_ID) return@launch tunnelManager.stopTunnel() - val tunnel = tunnelRepository.getById(tunnelId) - tunnelManager.stopTunnel(tunnel) - } - } - } - } + override fun onReceive(context: Context, intent: Intent) { + applicationScope.launch { + when (intent.action) { + NotificationAction.AUTO_TUNNEL_OFF.name -> serviceManager.stopAutoTunnel() + NotificationAction.TUNNEL_OFF.name -> { + val tunnelId = intent.getIntExtra(NotificationManager.EXTRA_ID, 0) + if (tunnelId == STOP_ALL_TUNNELS_ID) return@launch tunnelManager.stopTunnel() + val tunnel = tunnelRepository.getById(tunnelId) + tunnelManager.stopTunnel(tunnel) + } + } + } + } - companion object { - const val STOP_ALL_TUNNELS_ID = 0 - } + companion object { + const val STOP_ALL_TUNNELS_ID = 0 + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/broadcast/RemoteControlReceiver.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/broadcast/RemoteControlReceiver.kt index a0644c08..617ad340 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/broadcast/RemoteControlReceiver.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/broadcast/RemoteControlReceiver.kt @@ -9,83 +9,88 @@ import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.util.Constants import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject @AndroidEntryPoint class RemoteControlReceiver : BroadcastReceiver() { - @Inject - @ApplicationScope - lateinit var applicationScope: CoroutineScope + @Inject @ApplicationScope lateinit var applicationScope: CoroutineScope - @Inject - lateinit var appDataRepository: AppDataRepository + @Inject lateinit var appDataRepository: AppDataRepository - @Inject - lateinit var serviceManager: ServiceManager + @Inject lateinit var serviceManager: ServiceManager - @Inject - lateinit var tunnelManager: TunnelManager + @Inject lateinit var tunnelManager: TunnelManager - enum class Action(private val suffix: String) { - START_TUNNEL("START_TUNNEL"), - STOP_TUNNEL("STOP_TUNNEL"), - START_AUTO_TUNNEL("START_AUTO_TUNNEL"), - STOP_AUTO_TUNNEL("STOP_AUTO_TUNNEL"), - ; + enum class Action(private val suffix: String) { + START_TUNNEL("START_TUNNEL"), + STOP_TUNNEL("STOP_TUNNEL"), + START_AUTO_TUNNEL("START_AUTO_TUNNEL"), + STOP_AUTO_TUNNEL("STOP_AUTO_TUNNEL"); - fun getFullAction(): String { - return "${Constants.BASE_PACKAGE}.$suffix" - } + fun getFullAction(): String { + return "${Constants.BASE_PACKAGE}.$suffix" + } - companion object { - fun fromAction(action: String): Action? { - for (a in entries) { - if (a.getFullAction() == action) { - return a - } - } - return null - } - } - } + companion object { + fun fromAction(action: String): Action? { + for (a in entries) { + if (a.getFullAction() == action) { + return a + } + } + return null + } + } + } - override fun onReceive(context: Context, intent: Intent) { - Timber.i("onReceive") - val action = intent.action ?: return - val appAction = Action.fromAction(action) ?: return Timber.w("Unknown action $action") - applicationScope.launch { - if (!appDataRepository.appState.isRemoteControlEnabled()) return@launch Timber.w("Remote control disabled") - val key = appDataRepository.appState.getRemoteKey() ?: return@launch Timber.w("Remote control key missing") - if (key != intent.getStringExtra(EXTRA_KEY)?.trim()) return@launch Timber.w("Invalid remote control key") - when (appAction) { - Action.START_TUNNEL -> { - val tunnelName = intent.getStringExtra(EXTRA_TUN_NAME) ?: return@launch startDefaultTunnel() - val tunnel = appDataRepository.tunnels.findByTunnelName(tunnelName) ?: return@launch startDefaultTunnel() - tunnelManager.startTunnel(tunnel) - } - Action.STOP_TUNNEL -> { - val tunnelName = intent.getStringExtra(EXTRA_TUN_NAME) ?: return@launch tunnelManager.stopTunnel() - val tunnel = appDataRepository.tunnels.findByTunnelName(tunnelName) ?: return@launch tunnelManager.stopTunnel() - tunnelManager.stopTunnel(tunnel) - } - Action.START_AUTO_TUNNEL -> serviceManager.startAutoTunnel() - Action.STOP_AUTO_TUNNEL -> serviceManager.stopAutoTunnel() - } - } - } + override fun onReceive(context: Context, intent: Intent) { + Timber.i("onReceive") + val action = intent.action ?: return + val appAction = Action.fromAction(action) ?: return Timber.w("Unknown action $action") + applicationScope.launch { + if (!appDataRepository.appState.isRemoteControlEnabled()) + return@launch Timber.w("Remote control disabled") + val key = + appDataRepository.appState.getRemoteKey() + ?: return@launch Timber.w("Remote control key missing") + if (key != intent.getStringExtra(EXTRA_KEY)?.trim()) + return@launch Timber.w("Invalid remote control key") + when (appAction) { + Action.START_TUNNEL -> { + val tunnelName = + intent.getStringExtra(EXTRA_TUN_NAME) ?: return@launch startDefaultTunnel() + val tunnel = + appDataRepository.tunnels.findByTunnelName(tunnelName) + ?: return@launch startDefaultTunnel() + tunnelManager.startTunnel(tunnel) + } + Action.STOP_TUNNEL -> { + val tunnelName = + intent.getStringExtra(EXTRA_TUN_NAME) + ?: return@launch tunnelManager.stopTunnel() + val tunnel = + appDataRepository.tunnels.findByTunnelName(tunnelName) + ?: return@launch tunnelManager.stopTunnel() + tunnelManager.stopTunnel(tunnel) + } + Action.START_AUTO_TUNNEL -> serviceManager.startAutoTunnel() + Action.STOP_AUTO_TUNNEL -> serviceManager.stopAutoTunnel() + } + } + } - private suspend fun startDefaultTunnel() { - appDataRepository.getPrimaryOrFirstTunnel()?.let { tunnel -> - tunnelManager.startTunnel(tunnel) - } - } + private suspend fun startDefaultTunnel() { + appDataRepository.getPrimaryOrFirstTunnel()?.let { tunnel -> + tunnelManager.startTunnel(tunnel) + } + } - companion object { - const val EXTRA_TUN_NAME = "tunnelName" - const val EXTRA_KEY = "key" - } + companion object { + const val EXTRA_TUN_NAME = "tunnelName" + const val EXTRA_KEY = "key" + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/broadcast/RestartReceiver.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/broadcast/RestartReceiver.kt index 6249e872..b5b77d59 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/broadcast/RestartReceiver.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/broadcast/RestartReceiver.kt @@ -9,48 +9,41 @@ import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject @AndroidEntryPoint class RestartReceiver : BroadcastReceiver() { - @Inject - lateinit var appDataRepository: AppDataRepository + @Inject lateinit var appDataRepository: AppDataRepository - @Inject - @ApplicationScope - lateinit var applicationScope: CoroutineScope + @Inject @ApplicationScope lateinit var applicationScope: CoroutineScope - @Inject - lateinit var serviceManager: ServiceManager + @Inject lateinit var serviceManager: ServiceManager - @Inject - lateinit var tunnelManager: TunnelManager + @Inject lateinit var tunnelManager: TunnelManager - @Inject - @IoDispatcher - lateinit var ioDispatcher: CoroutineDispatcher + @Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher - override fun onReceive(context: Context, intent: Intent) { - Timber.d("RestartReceiver triggered with action: ${intent.action}") - serviceManager.updateTunnelTile() - serviceManager.updateAutoTunnelTile() - applicationScope.launch(ioDispatcher) { - val settings = appDataRepository.settings.get() - if (settings.isRestoreOnBootEnabled) { - if (settings.isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) { - Timber.d("Starting auto-tunnel on boot/update") - serviceManager.startAutoTunnel() - } else { - Timber.d("Restoring previous tunnel state") - tunnelManager.restorePreviousState() - } - } else { - Timber.d("Restore on boot disabled, skipping") - } - } - } + override fun onReceive(context: Context, intent: Intent) { + Timber.d("RestartReceiver triggered with action: ${intent.action}") + serviceManager.updateTunnelTile() + serviceManager.updateAutoTunnelTile() + applicationScope.launch(ioDispatcher) { + val settings = appDataRepository.settings.get() + if (settings.isRestoreOnBootEnabled) { + if (settings.isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) { + Timber.d("Starting auto-tunnel on boot/update") + serviceManager.startAutoTunnel() + } else { + Timber.d("Restoring previous tunnel state") + tunnelManager.restorePreviousState() + } + } else { + Timber.d("Restore on boot disabled, skipping") + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/NotificationManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/NotificationManager.kt index 715de11c..cfc4e5a2 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/NotificationManager.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/NotificationManager.kt @@ -9,38 +9,42 @@ import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction import com.zaneschepke.wireguardautotunnel.util.StringValue interface NotificationManager { - val context: Context - fun createNotification( - channel: NotificationChannels, - title: String = "", - actions: Collection = emptyList(), - description: String = "", - showTimestamp: Boolean = false, - importance: Int = NotificationManager.IMPORTANCE_HIGH, - onGoing: Boolean = true, - onlyAlertOnce: Boolean = true, - ): Notification + val context: Context - fun createNotification( - channel: NotificationChannels, - title: StringValue, - actions: Collection = emptyList(), - description: StringValue, - showTimestamp: Boolean = false, - importance: Int = NotificationManager.IMPORTANCE_HIGH, - onGoing: Boolean = true, - onlyAlertOnce: Boolean = true, - ): Notification + fun createNotification( + channel: NotificationChannels, + title: String = "", + actions: Collection = emptyList(), + description: String = "", + showTimestamp: Boolean = false, + importance: Int = NotificationManager.IMPORTANCE_HIGH, + onGoing: Boolean = true, + onlyAlertOnce: Boolean = true, + ): Notification - fun createNotificationAction(notificationAction: NotificationAction, extraId: Int? = null): NotificationCompat.Action + fun createNotification( + channel: NotificationChannels, + title: StringValue, + actions: Collection = emptyList(), + description: StringValue, + showTimestamp: Boolean = false, + importance: Int = NotificationManager.IMPORTANCE_HIGH, + onGoing: Boolean = true, + onlyAlertOnce: Boolean = true, + ): Notification - fun remove(notificationId: Int) + fun createNotificationAction( + notificationAction: NotificationAction, + extraId: Int? = null, + ): NotificationCompat.Action - fun show(notificationId: Int, notification: Notification) + fun remove(notificationId: Int) - companion object { - const val AUTO_TUNNEL_NOTIFICATION_ID = 122 - const val VPN_NOTIFICATION_ID = 100 - const val EXTRA_ID = "id" - } + fun show(notificationId: Int, notification: Notification) + + companion object { + const val AUTO_TUNNEL_NOTIFICATION_ID = 122 + const val VPN_NOTIFICATION_ID = 100 + const val EXTRA_ID = "id" + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/WireGuardNotification.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/WireGuardNotification.kt index 589761fa..a2bf8944 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/WireGuardNotification.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/WireGuardNotification.kt @@ -21,149 +21,156 @@ import com.zaneschepke.wireguardautotunnel.util.StringValue import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject -class WireGuardNotification -@Inject -constructor( - @ApplicationContext override val context: Context, -) : com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager { +class WireGuardNotification @Inject constructor(@ApplicationContext override val context: Context) : + com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager { - enum class NotificationChannels { - VPN, - AUTO_TUNNEL, - } + enum class NotificationChannels { + VPN, + AUTO_TUNNEL, + } - private val notificationManager = NotificationManagerCompat.from(context) + private val notificationManager = NotificationManagerCompat.from(context) - override fun createNotification( - channel: NotificationChannels, - title: String, - actions: Collection, - description: String, - showTimestamp: Boolean, - importance: Int, - onGoing: Boolean, - onlyAlertOnce: Boolean, - ): Notification { - notificationManager.createNotificationChannel(channel.asChannel()) - return channel.asBuilder().apply { - actions.forEach { - addAction(it) - } - setContentTitle(title) - setContentIntent( - PendingIntent.getActivity( - context, - 0, - Intent(context, MainActivity::class.java), - PendingIntent.FLAG_IMMUTABLE, - ), - ) - setContentText(description) - setOnlyAlertOnce(onlyAlertOnce) - setOngoing(onGoing) - setPriority(NotificationCompat.PRIORITY_HIGH) - setShowWhen(showTimestamp) - setSmallIcon(R.drawable.ic_launcher) - }.build() - } + override fun createNotification( + channel: NotificationChannels, + title: String, + actions: Collection, + description: String, + showTimestamp: Boolean, + importance: Int, + onGoing: Boolean, + onlyAlertOnce: Boolean, + ): Notification { + notificationManager.createNotificationChannel(channel.asChannel()) + return channel + .asBuilder() + .apply { + actions.forEach { addAction(it) } + setContentTitle(title) + setContentIntent( + PendingIntent.getActivity( + context, + 0, + Intent(context, MainActivity::class.java), + PendingIntent.FLAG_IMMUTABLE, + ) + ) + setContentText(description) + setOnlyAlertOnce(onlyAlertOnce) + setOngoing(onGoing) + setPriority(NotificationCompat.PRIORITY_HIGH) + setShowWhen(showTimestamp) + setSmallIcon(R.drawable.ic_launcher) + } + .build() + } - override fun createNotification( - channel: NotificationChannels, - title: StringValue, - actions: Collection, - description: StringValue, - showTimestamp: Boolean, - importance: Int, - onGoing: Boolean, - onlyAlertOnce: Boolean, - ): Notification { - return createNotification( - channel, - title.asString(context), - actions, - description.asString(context), - showTimestamp, - importance, - onGoing, - onlyAlertOnce, - ) - } + override fun createNotification( + channel: NotificationChannels, + title: StringValue, + actions: Collection, + description: StringValue, + showTimestamp: Boolean, + importance: Int, + onGoing: Boolean, + onlyAlertOnce: Boolean, + ): Notification { + return createNotification( + channel, + title.asString(context), + actions, + description.asString(context), + showTimestamp, + importance, + onGoing, + onlyAlertOnce, + ) + } - override fun createNotificationAction(notificationAction: NotificationAction, extraId: Int?): NotificationCompat.Action { - val pendingIntent = PendingIntent.getBroadcast( - context, - 0, - Intent(context, NotificationActionReceiver::class.java).apply { - action = notificationAction.name - if (extraId != null) putExtra(EXTRA_ID, extraId) - }, - PendingIntent.FLAG_IMMUTABLE, - ) - return NotificationCompat.Action.Builder( - R.drawable.ic_launcher, - notificationAction.title(context).uppercase(), - pendingIntent, - ).build() - } + override fun createNotificationAction( + notificationAction: NotificationAction, + extraId: Int?, + ): NotificationCompat.Action { + val pendingIntent = + PendingIntent.getBroadcast( + context, + 0, + Intent(context, NotificationActionReceiver::class.java).apply { + action = notificationAction.name + if (extraId != null) putExtra(EXTRA_ID, extraId) + }, + PendingIntent.FLAG_IMMUTABLE, + ) + return NotificationCompat.Action.Builder( + R.drawable.ic_launcher, + notificationAction.title(context).uppercase(), + pendingIntent, + ) + .build() + } - override fun remove(notificationId: Int) { - notificationManager.cancel(notificationId) - } + override fun remove(notificationId: Int) { + notificationManager.cancel(notificationId) + } - override fun show(notificationId: Int, notification: Notification) { - with(notificationManager) { - if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { - return - } - notify(notificationId, notification) - } - } + override fun show(notificationId: Int, notification: Notification) { + with(notificationManager) { + if ( + ActivityCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS, + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + notify(notificationId, notification) + } + } - private fun NotificationChannels.asBuilder(): NotificationCompat.Builder { - return when (this) { - NotificationChannels.AUTO_TUNNEL -> { - NotificationCompat.Builder( - context, - context.getString(R.string.auto_tunnel_channel_id), - ) - } - NotificationChannels.VPN -> { - NotificationCompat.Builder( - context, - context.getString(R.string.vpn_channel_id), - ) - } - } - } + private fun NotificationChannels.asBuilder(): NotificationCompat.Builder { + return when (this) { + NotificationChannels.AUTO_TUNNEL -> { + NotificationCompat.Builder( + context, + context.getString(R.string.auto_tunnel_channel_id), + ) + } + NotificationChannels.VPN -> { + NotificationCompat.Builder(context, context.getString(R.string.vpn_channel_id)) + } + } + } - private fun NotificationChannels.asChannel(): NotificationChannel { - return when (this) { - NotificationChannels.VPN -> { - NotificationChannel( - context.getString(R.string.vpn_channel_id), - context.getString(R.string.vpn_channel_name), - NotificationManager.IMPORTANCE_HIGH, - ).apply { - description = context.getString(R.string.vpn_channel_description) - enableLights(true) - lightColor = Color.WHITE - enableVibration(false) - vibrationPattern = longArrayOf(100, 200, 300) - } - } - NotificationChannels.AUTO_TUNNEL -> { - NotificationChannel( - context.getString(R.string.auto_tunnel_channel_id), - context.getString(R.string.auto_tunnel_channel_name), - NotificationManager.IMPORTANCE_HIGH, - ).apply { - description = context.getString(R.string.auto_tunnel_channel_description) - enableLights(true) - lightColor = Color.WHITE - enableVibration(false) - vibrationPattern = longArrayOf(100, 200, 300) - } - } - } - } + private fun NotificationChannels.asChannel(): NotificationChannel { + return when (this) { + NotificationChannels.VPN -> { + NotificationChannel( + context.getString(R.string.vpn_channel_id), + context.getString(R.string.vpn_channel_name), + NotificationManager.IMPORTANCE_HIGH, + ) + .apply { + description = context.getString(R.string.vpn_channel_description) + enableLights(true) + lightColor = Color.WHITE + enableVibration(false) + vibrationPattern = longArrayOf(100, 200, 300) + } + } + NotificationChannels.AUTO_TUNNEL -> { + NotificationChannel( + context.getString(R.string.auto_tunnel_channel_id), + context.getString(R.string.auto_tunnel_channel_name), + NotificationManager.IMPORTANCE_HIGH, + ) + .apply { + description = context.getString(R.string.auto_tunnel_channel_description) + enableLights(true) + lightColor = Color.WHITE + enableVibration(false) + vibrationPattern = longArrayOf(100, 200, 300) + } + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/ServiceManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/ServiceManager.kt index 68e6bd34..69a387fa 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/ServiceManager.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/ServiceManager.kt @@ -25,106 +25,110 @@ import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import timber.log.Timber -class ServiceManager @Inject constructor( - private val context: Context, - @IoDispatcher private val ioDispatcher: CoroutineDispatcher, - @ApplicationScope private val applicationScope: CoroutineScope, - @MainDispatcher private val mainDispatcher: CoroutineDispatcher, - private val appDataRepository: AppDataRepository, +class ServiceManager +@Inject +constructor( + private val context: Context, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + @ApplicationScope private val applicationScope: CoroutineScope, + @MainDispatcher private val mainDispatcher: CoroutineDispatcher, + private val appDataRepository: AppDataRepository, ) { - private val autoTunnelMutex = Mutex() + private val autoTunnelMutex = Mutex() - private val _autoTunnelActive = MutableStateFlow(false) - val autoTunnelActive = _autoTunnelActive.asStateFlow() + private val _autoTunnelActive = MutableStateFlow(false) + val autoTunnelActive = _autoTunnelActive.asStateFlow() - var autoTunnelService = CompletableDeferred() - var backgroundService = CompletableDeferred() + var autoTunnelService = CompletableDeferred() + var backgroundService = CompletableDeferred() - private fun startService(cls: Class, background: Boolean) { - runCatching { - val intent = Intent(context, cls) - if (background) { - context.startForegroundService(intent) - } else { - context.startService(intent) - } - }.onFailure { Timber.e(it) } - } + private fun startService(cls: Class, background: Boolean) { + runCatching { + val intent = Intent(context, cls) + if (background) { + context.startForegroundService(intent) + } else { + context.startService(intent) + } + } + .onFailure { Timber.e(it) } + } - fun hasVpnPermission(): Boolean { - return VpnService.prepare(context) == null - } + fun hasVpnPermission(): Boolean { + return VpnService.prepare(context) == null + } - suspend fun startAutoTunnel() { - autoTunnelMutex.withLock { - val settings = appDataRepository.settings.get() - appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true)) - if (autoTunnelService.isCompleted) { - _autoTunnelActive.update { true } - return - } - runCatching { - autoTunnelService = CompletableDeferred() - startService(AutoTunnelService::class.java, !WireGuardAutoTunnel.isForeground()) - _autoTunnelActive.update { true } - }.onFailure { - Timber.e(it) - _autoTunnelActive.update { false } - } - withContext(mainDispatcher) { updateAutoTunnelTile() } - } - } + suspend fun startAutoTunnel() { + autoTunnelMutex.withLock { + val settings = appDataRepository.settings.get() + appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true)) + if (autoTunnelService.isCompleted) { + _autoTunnelActive.update { true } + return + } + runCatching { + autoTunnelService = CompletableDeferred() + startService(AutoTunnelService::class.java, !WireGuardAutoTunnel.isForeground()) + _autoTunnelActive.update { true } + } + .onFailure { + Timber.e(it) + _autoTunnelActive.update { false } + } + withContext(mainDispatcher) { updateAutoTunnelTile() } + } + } - suspend fun stopAutoTunnel() { - autoTunnelMutex.withLock { - val settings = appDataRepository.settings.get() - appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = false)) - if (!autoTunnelService.isCompleted) return - runCatching { - val service = autoTunnelService.await() - service.stop() - _autoTunnelActive.update { false } - autoTunnelService = CompletableDeferred() - }.onFailure { - Timber.e(it) - } - withContext(mainDispatcher) { updateAutoTunnelTile() } - } - } + suspend fun stopAutoTunnel() { + autoTunnelMutex.withLock { + val settings = appDataRepository.settings.get() + appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = false)) + if (!autoTunnelService.isCompleted) return + runCatching { + val service = autoTunnelService.await() + service.stop() + _autoTunnelActive.update { false } + autoTunnelService = CompletableDeferred() + } + .onFailure { Timber.e(it) } + withContext(mainDispatcher) { updateAutoTunnelTile() } + } + } - fun startTunnelForegroundService() { - if (backgroundService.isCompleted) return - runCatching { - backgroundService = CompletableDeferred() - startService(TunnelForegroundService::class.java, !WireGuardAutoTunnel.isForeground()) - }.onFailure { - Timber.e(it) - } - } + fun startTunnelForegroundService() { + if (backgroundService.isCompleted) return + runCatching { + backgroundService = CompletableDeferred() + startService( + TunnelForegroundService::class.java, + !WireGuardAutoTunnel.isForeground(), + ) + } + .onFailure { Timber.e(it) } + } - suspend fun stopTunnelForegroundService() { - if (!backgroundService.isCompleted) return - runCatching { - val service = backgroundService.await() - service.stop() - backgroundService = CompletableDeferred() - }.onFailure { - Timber.e(it) - } - } + suspend fun stopTunnelForegroundService() { + if (!backgroundService.isCompleted) return + runCatching { + val service = backgroundService.await() + service.stop() + backgroundService = CompletableDeferred() + } + .onFailure { Timber.e(it) } + } - fun toggleAutoTunnel() { - applicationScope.launch(ioDispatcher) { - if (_autoTunnelActive.value) stopAutoTunnel() else startAutoTunnel() - } - } + fun toggleAutoTunnel() { + applicationScope.launch(ioDispatcher) { + if (_autoTunnelActive.value) stopAutoTunnel() else startAutoTunnel() + } + } - fun updateAutoTunnelTile() { - context.requestAutoTunnelTileServiceUpdate() - } + fun updateAutoTunnelTile() { + context.requestAutoTunnelTileServiceUpdate() + } - fun updateTunnelTile() { - context.requestTunnelTileServiceStateUpdate() - } + fun updateTunnelTile() { + context.requestTunnelTileServiceStateUpdate() + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/TunnelForegroundService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/TunnelForegroundService.kt index f4b4b375..05950575 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/TunnelForegroundService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/TunnelForegroundService.kt @@ -20,6 +20,8 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.extensions.distinctByKeys import dagger.hilt.android.AndroidEntryPoint +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Job @@ -36,237 +38,235 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber -import java.util.concurrent.ConcurrentHashMap -import javax.inject.Inject @AndroidEntryPoint class TunnelForegroundService : LifecycleService() { - @Inject - lateinit var notificationManager: NotificationManager + @Inject lateinit var notificationManager: NotificationManager - @Inject - lateinit var serviceManager: ServiceManager + @Inject lateinit var serviceManager: ServiceManager - @Inject - lateinit var networkMonitor: NetworkMonitor + @Inject lateinit var networkMonitor: NetworkMonitor - @Inject - @IoDispatcher - lateinit var ioDispatcher: CoroutineDispatcher + @Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher - @Inject - lateinit var tunnelRepo: TunnelRepository + @Inject lateinit var tunnelRepo: TunnelRepository - @Inject - lateinit var tunnelManager: TunnelManager + @Inject lateinit var tunnelManager: TunnelManager - private val isNetworkConnected = MutableStateFlow(true) + private val isNetworkConnected = MutableStateFlow(true) - private val tunnelJobs = ConcurrentHashMap() + private val tunnelJobs = ConcurrentHashMap() - override fun onCreate() { - super.onCreate() - serviceManager.backgroundService.complete(this) - ServiceCompat.startForeground( - this@TunnelForegroundService, - NotificationManager.VPN_NOTIFICATION_ID, - onCreateNotification(), - Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID, - ) - } + override fun onCreate() { + super.onCreate() + serviceManager.backgroundService.complete(this) + ServiceCompat.startForeground( + this@TunnelForegroundService, + NotificationManager.VPN_NOTIFICATION_ID, + onCreateNotification(), + Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID, + ) + } - override fun onBind(intent: Intent): IBinder? { - super.onBind(intent) - return null - } + override fun onBind(intent: Intent): IBinder? { + super.onBind(intent) + return null + } - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - super.onStartCommand(intent, flags, startId) - serviceManager.backgroundService.complete(this) - ServiceCompat.startForeground( - this@TunnelForegroundService, - NotificationManager.VPN_NOTIFICATION_ID, - onCreateNotification(), - Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID, - ) - start() - return START_STICKY - } + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + super.onStartCommand(intent, flags, startId) + serviceManager.backgroundService.complete(this) + ServiceCompat.startForeground( + this@TunnelForegroundService, + NotificationManager.VPN_NOTIFICATION_ID, + onCreateNotification(), + Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID, + ) + start() + return START_STICKY + } - fun start() = lifecycleScope.launch { - tunnelManager.activeTunnels.distinctByKeys().collect { tuns -> - if (tuns.isEmpty() && tunnelJobs.isEmpty()) return@collect - if (tuns.isEmpty() && tunnelJobs.isNotEmpty()) { - return@collect tunnelJobs.forEach { (key, _) -> - Timber.d("Stopping all tunnel jobs") - tunnelJobs[key]?.cancel() - tunnelJobs.remove(key) - } - } - val (jobsToStop, jobsToStart) = findMissingKeys(tuns, tunnelJobs) - if (jobsToStop.isEmpty() && jobsToStart.isEmpty()) return@collect - jobsToStop.forEach { tun -> - Timber.d("Stopping tunnel jobs for ${tun.tunName}") - tunnelJobs[tun]?.cancel() - tunnelJobs.remove(tun) - } - jobsToStart.forEach { tun -> - Timber.d("Starting tunnel jobs for ${tun.tunName}") - tunnelJobs += (tun to startTunnelJobs(tun)) - } - updateServiceNotification() - } - } + fun start() = + lifecycleScope.launch { + tunnelManager.activeTunnels.distinctByKeys().collect { tuns -> + if (tuns.isEmpty() && tunnelJobs.isEmpty()) return@collect + if (tuns.isEmpty() && tunnelJobs.isNotEmpty()) { + return@collect tunnelJobs.forEach { (key, _) -> + Timber.d("Stopping all tunnel jobs") + tunnelJobs[key]?.cancel() + tunnelJobs.remove(key) + } + } + val (jobsToStop, jobsToStart) = findMissingKeys(tuns, tunnelJobs) + if (jobsToStop.isEmpty() && jobsToStart.isEmpty()) return@collect + jobsToStop.forEach { tun -> + Timber.d("Stopping tunnel jobs for ${tun.tunName}") + tunnelJobs[tun]?.cancel() + tunnelJobs.remove(tun) + } + jobsToStart.forEach { tun -> + Timber.d("Starting tunnel jobs for ${tun.tunName}") + tunnelJobs += (tun to startTunnelJobs(tun)) + } + updateServiceNotification() + } + } - // TODO Would be cool to have this include kill switch - // TODO also we need to include errors - private fun updateServiceNotification() { - val notification = when (tunnelJobs.size) { - 0 -> onCreateNotification() - 1 -> createTunnelNotification(tunnelJobs.keys.first()) - else -> createTunnelsNotification() - } - ServiceCompat.startForeground( - this@TunnelForegroundService, - NotificationManager.VPN_NOTIFICATION_ID, - notification, - Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID, - ) - } + // TODO Would be cool to have this include kill switch + // TODO also we need to include errors + private fun updateServiceNotification() { + val notification = + when (tunnelJobs.size) { + 0 -> onCreateNotification() + 1 -> createTunnelNotification(tunnelJobs.keys.first()) + else -> createTunnelsNotification() + } + ServiceCompat.startForeground( + this@TunnelForegroundService, + NotificationManager.VPN_NOTIFICATION_ID, + notification, + Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID, + ) + } - // use same scope so we can cancel all of these - private fun startTunnelJobs(tunnelConf: TunnelConf) = lifecycleScope.launch { - // monitor if we have internet connectivity - launch { startNetworkMonitorJob() } - // job to trigger stats emit on interval - launch { startTunnelStatsJob(tunnelConf) } - // monitor changes to the tunnel config - launch { startTunnelConfChangesJob(tunnelConf) } - // monitor tunnel ping - launch { startPingJob(tunnelConf) } - } + // use same scope so we can cancel all of these + private fun startTunnelJobs(tunnelConf: TunnelConf) = + lifecycleScope.launch { + // monitor if we have internet connectivity + launch { startNetworkMonitorJob() } + // job to trigger stats emit on interval + launch { startTunnelStatsJob(tunnelConf) } + // monitor changes to the tunnel config + launch { startTunnelConfChangesJob(tunnelConf) } + // monitor tunnel ping + launch { startPingJob(tunnelConf) } + } - private fun findMissingKeys(map1: Map, map2: Map): Pair, Set> { - val missingMap1 = map2.keys - map1.keys - val missingMap2 = map1.keys - map2.keys - return missingMap1 to missingMap2 - } + private fun findMissingKeys( + map1: Map, + map2: Map, + ): Pair, Set> { + val missingMap1 = map2.keys - map1.keys + val missingMap2 = map1.keys - map2.keys + return missingMap1 to missingMap2 + } - private suspend fun startTunnelConfChangesJob(tunnelConf: TunnelConf) { - tunnelRepo.flow - .flowOn(ioDispatcher) - .map { storedTunnels -> - storedTunnels.firstOrNull { it.id == tunnelConf.id } - } - .filterNotNull() - // only emit when one of these 3 values change - .distinctUntilChanged { old, new -> - old == new - } - .collect { storedTunnel -> - if (tunnelConf != storedTunnel) { - Timber.d("Config changed for ${storedTunnel.tunName}, bouncing") - // let this complete, even after cancel - withContext(NonCancellable) { - tunnelManager.bounceTunnel(storedTunnel, TunnelStatus.StopReason.CONFIG_CHANGED) - } - } - } - } + private suspend fun startTunnelConfChangesJob(tunnelConf: TunnelConf) { + tunnelRepo.flow + .flowOn(ioDispatcher) + .map { storedTunnels -> storedTunnels.firstOrNull { it.id == tunnelConf.id } } + .filterNotNull() + // only emit when one of these 3 values change + .distinctUntilChanged { old, new -> old == new } + .collect { storedTunnel -> + if (tunnelConf != storedTunnel) { + Timber.d("Config changed for ${storedTunnel.tunName}, bouncing") + // let this complete, even after cancel + withContext(NonCancellable) { + tunnelManager.bounceTunnel( + storedTunnel, + TunnelStatus.StopReason.CONFIG_CHANGED, + ) + } + } + } + } - private suspend fun startNetworkMonitorJob() { - networkMonitor.networkStatusFlow - .flowOn(ioDispatcher) - .collectLatest { status -> - val isAvailable = status !is NetworkStatus.Disconnected - isNetworkConnected.value = isAvailable - Timber.d("Network available: $status") - } - } + private suspend fun startNetworkMonitorJob() { + networkMonitor.networkStatusFlow.flowOn(ioDispatcher).collectLatest { status -> + val isAvailable = status !is NetworkStatus.Disconnected + isNetworkConnected.value = isAvailable + Timber.d("Network available: $status") + } + } - private suspend fun startTunnelStatsJob(tunnel: TunnelConf) = coroutineScope { - while (isActive) { - tunnelManager.updateTunnelStatistics(tunnel) - delay(STATS_DELAY) - } - } + private suspend fun startTunnelStatsJob(tunnel: TunnelConf) = coroutineScope { + while (isActive) { + tunnelManager.updateTunnelStatistics(tunnel) + delay(STATS_DELAY) + } + } - // TODO fix cooldown - private suspend fun startPingJob(tunnel: TunnelConf) = coroutineScope { - delay(PING_START_DELAY) - while (isActive) { - val shouldBounce = shouldBounceTunnel(tunnel) - val delayMs = if (shouldBounce) { - // let this complete, even after cancel - withContext(NonCancellable) { - tunnelManager.bounceTunnel(tunnel, TunnelStatus.StopReason.PING) - } - tunnel.pingCooldown ?: Constants.PING_COOLDOWN - } else { - tunnel.pingInterval ?: Constants.PING_INTERVAL - } - delay(delayMs) - } - } + // TODO fix cooldown + private suspend fun startPingJob(tunnel: TunnelConf) = coroutineScope { + delay(PING_START_DELAY) + while (isActive) { + val shouldBounce = shouldBounceTunnel(tunnel) + val delayMs = + if (shouldBounce) { + // let this complete, even after cancel + withContext(NonCancellable) { + tunnelManager.bounceTunnel(tunnel, TunnelStatus.StopReason.PING) + } + tunnel.pingCooldown ?: Constants.PING_COOLDOWN + } else { + tunnel.pingInterval ?: Constants.PING_INTERVAL + } + delay(delayMs) + } + } - private suspend fun shouldBounceTunnel(tunnel: TunnelConf): Boolean { - if (!isNetworkConnected.value) { - Timber.d("Network disconnected, skipping ping for ${tunnel.tunName}") - return false - } - return runCatching { - !tunnel.isTunnelPingable(ioDispatcher) - }.onFailure { e -> - Timber.e(e, "Ping check failed for ${tunnel.tunName}") - }.getOrDefault(true) - } + private suspend fun shouldBounceTunnel(tunnel: TunnelConf): Boolean { + if (!isNetworkConnected.value) { + Timber.d("Network disconnected, skipping ping for ${tunnel.tunName}") + return false + } + return runCatching { !tunnel.isTunnelPingable(ioDispatcher) } + .onFailure { e -> Timber.e(e, "Ping check failed for ${tunnel.tunName}") } + .getOrDefault(true) + } - fun stop() { - ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) - stopSelf() - } + fun stop() { + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) + stopSelf() + } - override fun onDestroy() { - serviceManager.backgroundService = CompletableDeferred() - ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) - super.onDestroy() - } + override fun onDestroy() { + serviceManager.backgroundService = CompletableDeferred() + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) + super.onDestroy() + } - private fun createTunnelNotification(tunnelConf: TunnelConf): Notification { - return notificationManager.createNotification( - WireGuardNotification.NotificationChannels.VPN, - title = "${getString(R.string.tunnel_running)} - ${tunnelConf.tunName}", - actions = listOf( - notificationManager.createNotificationAction(NotificationAction.TUNNEL_OFF, tunnelConf.id), - ), - ) - } + private fun createTunnelNotification(tunnelConf: TunnelConf): Notification { + return notificationManager.createNotification( + WireGuardNotification.NotificationChannels.VPN, + title = "${getString(R.string.tunnel_running)} - ${tunnelConf.tunName}", + actions = + listOf( + notificationManager.createNotificationAction( + NotificationAction.TUNNEL_OFF, + tunnelConf.id, + ) + ), + ) + } - private fun createTunnelsNotification(): Notification { - return notificationManager.createNotification( - WireGuardNotification.NotificationChannels.VPN, - title = "${getString(R.string.tunnel_running)} - ${getString(R.string.multiple)}", - actions = listOf( - notificationManager.createNotificationAction(NotificationAction.TUNNEL_OFF, 0), - ), - ) - } + private fun createTunnelsNotification(): Notification { + return notificationManager.createNotification( + WireGuardNotification.NotificationChannels.VPN, + title = "${getString(R.string.tunnel_running)} - ${getString(R.string.multiple)}", + actions = + listOf( + notificationManager.createNotificationAction(NotificationAction.TUNNEL_OFF, 0) + ), + ) + } - private fun onCreateNotification(): Notification { - return notificationManager.createNotification( - WireGuardNotification.NotificationChannels.VPN, - title = getString(R.string.tunnel_starting), - ) - } + private fun onCreateNotification(): Notification { + return notificationManager.createNotification( + WireGuardNotification.NotificationChannels.VPN, + title = getString(R.string.tunnel_starting), + ) + } - // TODO add notification handling and optional log reading for restart on handshake failures - companion object { - const val STATS_DELAY = 1_000L - const val PING_START_DELAY = 30_000L - // ipv6 disabled or block on network -// const val userspaceStartFailed = "Failed to send handshake initiation: write udp [::]" -// const val ipv6Fails = "Failed to send data packets: write udp [::]" -// const val ipv4Fails = "Failed to send data packets: write udp 0.0.0.0:51820" - } + // TODO add notification handling and optional log reading for restart on handshake failures + companion object { + const val STATS_DELAY = 1_000L + const val PING_START_DELAY = 30_000L + // ipv6 disabled or block on network + // const val userspaceStartFailed = "Failed to send handshake initiation: write udp [::]" + // const val ipv6Fails = "Failed to send data packets: write udp [::]" + // const val ipv4Fails = "Failed to send data packets: write udp 0.0.0.0:51820" + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/autotunnel/AutoTunnelService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/autotunnel/AutoTunnelService.kt index bf1f98e2..831fae5f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/autotunnel/AutoTunnelService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/autotunnel/AutoTunnelService.kt @@ -26,6 +26,8 @@ import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import javax.inject.Provider import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -42,216 +44,233 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject -import javax.inject.Provider @AndroidEntryPoint class AutoTunnelService : LifecycleService() { - @Inject - lateinit var networkMonitor: NetworkMonitor + @Inject lateinit var networkMonitor: NetworkMonitor - @Inject - lateinit var appDataRepository: Provider + @Inject lateinit var appDataRepository: Provider - @Inject - lateinit var notificationManager: NotificationManager + @Inject lateinit var notificationManager: NotificationManager - @Inject - @IoDispatcher - lateinit var ioDispatcher: CoroutineDispatcher + @Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher - @Inject - lateinit var serviceManager: ServiceManager + @Inject lateinit var serviceManager: ServiceManager - @Inject - lateinit var tunnelManager: TunnelManager + @Inject lateinit var tunnelManager: TunnelManager - private val defaultState = AutoTunnelState() + private val defaultState = AutoTunnelState() - private val autoTunnelStateFlow = MutableStateFlow(defaultState) + private val autoTunnelStateFlow = MutableStateFlow(defaultState) - private var wakeLock: PowerManager.WakeLock? = null + private var wakeLock: PowerManager.WakeLock? = null - private var killSwitchJob: Job? = null + private var killSwitchJob: Job? = null - override fun onCreate() { - super.onCreate() - serviceManager.autoTunnelService.complete(this) - launchWatcherNotification() - } + override fun onCreate() { + super.onCreate() + serviceManager.autoTunnelService.complete(this) + launchWatcherNotification() + } - override fun onBind(intent: Intent): IBinder? { - super.onBind(intent) - return null - } + override fun onBind(intent: Intent): IBinder? { + super.onBind(intent) + return null + } - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - super.onStartCommand(intent, flags, startId) - Timber.d("onStartCommand executed with startId: $startId") - serviceManager.autoTunnelService.complete(this) - start() - return START_STICKY - } + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + super.onStartCommand(intent, flags, startId) + Timber.d("onStartCommand executed with startId: $startId") + serviceManager.autoTunnelService.complete(this) + start() + return START_STICKY + } - fun start() { - kotlin.runCatching { - launchWatcherNotification() - initWakeLock() - startAutoTunnelJob() - startAutoTunnelStateJob() - killSwitchJob = startKillSwitchJob() - }.onFailure { - Timber.e(it) - } - } + fun start() { + kotlin + .runCatching { + launchWatcherNotification() + initWakeLock() + startAutoTunnelJob() + startAutoTunnelStateJob() + killSwitchJob = startKillSwitchJob() + } + .onFailure { Timber.e(it) } + } - fun stop() { - wakeLock?.let { if (it.isHeld) it.release() } - stopSelf() - } + fun stop() { + wakeLock?.let { if (it.isHeld) it.release() } + stopSelf() + } - override fun onDestroy() { - serviceManager.autoTunnelService = CompletableDeferred() - restoreVpnKillSwitch() - super.onDestroy() - } + override fun onDestroy() { + serviceManager.autoTunnelService = CompletableDeferred() + restoreVpnKillSwitch() + super.onDestroy() + } - private fun restoreVpnKillSwitch() { - with(autoTunnelStateFlow.value) { - if (settings.isVpnKillSwitchEnabled && tunnelManager.getBackendState() != BackendState.KILL_SWITCH_ACTIVE) { - killSwitchJob?.cancel() - val allowedIps = if (settings.isLanOnKillSwitchEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS else emptyList() - tunnelManager.setBackendState(BackendState.KILL_SWITCH_ACTIVE, allowedIps) - } - } - } + private fun restoreVpnKillSwitch() { + with(autoTunnelStateFlow.value) { + if ( + settings.isVpnKillSwitchEnabled && + tunnelManager.getBackendState() != BackendState.KILL_SWITCH_ACTIVE + ) { + killSwitchJob?.cancel() + val allowedIps = + if (settings.isLanOnKillSwitchEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS + else emptyList() + tunnelManager.setBackendState(BackendState.KILL_SWITCH_ACTIVE, allowedIps) + } + } + } - private fun launchWatcherNotification(description: String = getString(R.string.monitoring_state_changes)) { - val notification = - notificationManager.createNotification( - WireGuardNotification.NotificationChannels.AUTO_TUNNEL, - title = getString(R.string.auto_tunnel_title), - description = description, - actions = listOf( - notificationManager.createNotificationAction(NotificationAction.AUTO_TUNNEL_OFF), - ), - ) - ServiceCompat.startForeground( - this, - NotificationManager.AUTO_TUNNEL_NOTIFICATION_ID, - notification, - Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID, - ) - } + private fun launchWatcherNotification( + description: String = getString(R.string.monitoring_state_changes) + ) { + val notification = + notificationManager.createNotification( + WireGuardNotification.NotificationChannels.AUTO_TUNNEL, + title = getString(R.string.auto_tunnel_title), + description = description, + actions = + listOf( + notificationManager.createNotificationAction( + NotificationAction.AUTO_TUNNEL_OFF + ) + ), + ) + ServiceCompat.startForeground( + this, + NotificationManager.AUTO_TUNNEL_NOTIFICATION_ID, + notification, + Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID, + ) + } - private fun initWakeLock() { - wakeLock = (getSystemService(POWER_SERVICE) as PowerManager).run { - val tag = this.javaClass.name - newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply { - try { - Timber.i("Initiating wakelock with 10 min timeout") - acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT) - } finally { - release() - } - } - } - } + private fun initWakeLock() { + wakeLock = + (getSystemService(POWER_SERVICE) as PowerManager).run { + val tag = this.javaClass.name + newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply { + try { + Timber.i("Initiating wakelock with 10 min timeout") + acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT) + } finally { + release() + } + } + } + } - private fun buildNetworkState(networkStatus: NetworkStatus): NetworkState { - return with(autoTunnelStateFlow.value.networkState) { - val wifiName = when (networkStatus) { - is NetworkStatus.Connected -> { - networkStatus.wifiSsid - } - else -> null - } - copy( - isWifiConnected = networkStatus.wifiConnected, - isMobileDataConnected = networkStatus.cellularConnected, - isEthernetConnected = networkStatus.ethernetConnected, - wifiName = wifiName, - ) - } - } + private fun buildNetworkState(networkStatus: NetworkStatus): NetworkState { + return with(autoTunnelStateFlow.value.networkState) { + val wifiName = + when (networkStatus) { + is NetworkStatus.Connected -> { + networkStatus.wifiSsid + } + else -> null + } + copy( + isWifiConnected = networkStatus.wifiConnected, + isMobileDataConnected = networkStatus.cellularConnected, + isEthernetConnected = networkStatus.ethernetConnected, + wifiName = wifiName, + ) + } + } - @OptIn(ExperimentalCoroutinesApi::class) - private fun startAutoTunnelStateJob() = lifecycleScope.launch(ioDispatcher) { - combine( - combineSettings(), - appDataRepository.get().settings.flow - .distinctUntilChanged { old, new -> old.isKernelEnabled == new.isKernelEnabled } // Only emit when isKernelEnabled changes - .flatMapLatest { - networkMonitor.networkStatusFlow - .flowOn(ioDispatcher) - .map { buildNetworkState(it) } - } - .distinctUntilChanged(), - ) { double, networkState -> - AutoTunnelState( - tunnelManager.activeTunnels.value, - networkState, - double.first, - double.second, - ) - }.collect { state -> - autoTunnelStateFlow.update { - it.copy( - activeTunnels = state.activeTunnels, - networkState = state.networkState, - settings = state.settings, - tunnels = state.tunnels, - ) - } - } - } + @OptIn(ExperimentalCoroutinesApi::class) + private fun startAutoTunnelStateJob() = + lifecycleScope.launch(ioDispatcher) { + combine( + combineSettings(), + appDataRepository + .get() + .settings + .flow + .distinctUntilChanged { old, new -> + old.isKernelEnabled == new.isKernelEnabled + } // Only emit when isKernelEnabled changes + .flatMapLatest { + networkMonitor.networkStatusFlow.flowOn(ioDispatcher).map { + buildNetworkState(it) + } + } + .distinctUntilChanged(), + ) { double, networkState -> + AutoTunnelState( + tunnelManager.activeTunnels.value, + networkState, + double.first, + double.second, + ) + } + .collect { state -> + autoTunnelStateFlow.update { + it.copy( + activeTunnels = state.activeTunnels, + networkState = state.networkState, + settings = state.settings, + tunnels = state.tunnels, + ) + } + } + } - private fun combineSettings(): Flow> { - return combine( - appDataRepository.get().settings.flow, - appDataRepository.get().tunnels.flow.map { tunnels -> - // isActive is ignored for equality checks so user can manually toggle off tunnel with auto-tunnel - tunnels.map { it.copy(isActive = false) } - }, - ) { settings, tunnels -> - Pair(settings, tunnels) - }.distinctUntilChanged() - } + private fun combineSettings(): Flow> { + return combine( + appDataRepository.get().settings.flow, + appDataRepository.get().tunnels.flow.map { tunnels -> + // isActive is ignored for equality checks so user can manually toggle off + // tunnel with auto-tunnel + tunnels.map { it.copy(isActive = false) } + }, + ) { settings, tunnels -> + Pair(settings, tunnels) + } + .distinctUntilChanged() + } - private fun startKillSwitchJob() = lifecycleScope.launch(ioDispatcher) { - autoTunnelStateFlow.collect { - if (it == defaultState) return@collect - when (val event = it.asKillSwitchEvent()) { - KillSwitchEvent.DoNothing -> Unit - is KillSwitchEvent.Start -> { - Timber.d("Starting kill switch") - tunnelManager.setBackendState(BackendState.KILL_SWITCH_ACTIVE, event.allowedIps) - } - KillSwitchEvent.Stop -> { - Timber.d("Stopping kill switch") - tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptySet()) - } - } - } - } + private fun startKillSwitchJob() = + lifecycleScope.launch(ioDispatcher) { + autoTunnelStateFlow.collect { + if (it == defaultState) return@collect + when (val event = it.asKillSwitchEvent()) { + KillSwitchEvent.DoNothing -> Unit + is KillSwitchEvent.Start -> { + Timber.d("Starting kill switch") + tunnelManager.setBackendState( + BackendState.KILL_SWITCH_ACTIVE, + event.allowedIps, + ) + } + KillSwitchEvent.Stop -> { + Timber.d("Stopping kill switch") + tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptySet()) + } + } + } + } - @OptIn(FlowPreview::class) - private fun startAutoTunnelJob() = lifecycleScope.launch(ioDispatcher) { - Timber.i("Starting auto-tunnel network event watcher") - val settings = appDataRepository.get().settings.get() - Timber.d("Starting with debounce delay of: ${settings.debounceDelaySeconds} seconds") - autoTunnelStateFlow.debounce(settings.debounceDelayMillis()).collect { watcherState -> - if (watcherState == defaultState) return@collect - Timber.d("New auto tunnel state emitted ${watcherState.networkState}") - when (val event = watcherState.asAutoTunnelEvent()) { - is AutoTunnelEvent.Start -> (event.tunnelConf ?: appDataRepository.get().getPrimaryOrFirstTunnel())?.let { - tunnelManager.startTunnel(it) - } - // TODO improve this to target specific tunnels to better support multi-tunnel - is AutoTunnelEvent.Stop -> tunnelManager.stopTunnel() - AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: no condition met") - } - } - } + @OptIn(FlowPreview::class) + private fun startAutoTunnelJob() = + lifecycleScope.launch(ioDispatcher) { + Timber.i("Starting auto-tunnel network event watcher") + val settings = appDataRepository.get().settings.get() + Timber.d("Starting with debounce delay of: ${settings.debounceDelaySeconds} seconds") + autoTunnelStateFlow.debounce(settings.debounceDelayMillis()).collect { watcherState -> + if (watcherState == defaultState) return@collect + Timber.d("New auto tunnel state emitted ${watcherState.networkState}") + when (val event = watcherState.asAutoTunnelEvent()) { + is AutoTunnelEvent.Start -> + (event.tunnelConf ?: appDataRepository.get().getPrimaryOrFirstTunnel()) + ?.let { tunnelManager.startTunnel(it) } + // TODO improve this to target specific tunnels to better support multi-tunnel + is AutoTunnelEvent.Stop -> tunnelManager.stopTunnel() + AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: no condition met") + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/tile/AutoTunnelControlTile.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/tile/AutoTunnelControlTile.kt index 907d3c08..df3cf46a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/tile/AutoTunnelControlTile.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/tile/AutoTunnelControlTile.kt @@ -11,96 +11,94 @@ import androidx.lifecycle.lifecycleScope import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject @AndroidEntryPoint class AutoTunnelControlTile : TileService(), LifecycleOwner { - @Inject - lateinit var appDataRepository: AppDataRepository + @Inject lateinit var appDataRepository: AppDataRepository - @Inject - lateinit var serviceManager: ServiceManager + @Inject lateinit var serviceManager: ServiceManager - private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this) + private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this) - override fun onCreate() { - super.onCreate() - lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) - } + override fun onCreate() { + super.onCreate() + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) + } - override fun onDestroy() { - super.onDestroy() - lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) - } + override fun onDestroy() { + super.onDestroy() + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) + } - override fun onStartListening() { - super.onStartListening() - lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) - Timber.d("Start listening called for auto tunnel tile") - lifecycleScope.launch { - serviceManager.autoTunnelActive.collect { - if (it) return@collect setActive() - setInactive() - } - } - lifecycleScope.launch { - appDataRepository.tunnels.flow.collect { - if (it.isEmpty()) { - setUnavailable() - } - } - } - } + override fun onStartListening() { + super.onStartListening() + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) + Timber.d("Start listening called for auto tunnel tile") + lifecycleScope.launch { + serviceManager.autoTunnelActive.collect { + if (it) return@collect setActive() + setInactive() + } + } + lifecycleScope.launch { + appDataRepository.tunnels.flow.collect { + if (it.isEmpty()) { + setUnavailable() + } + } + } + } - override fun onClick() { - super.onClick() - unlockAndRun { - lifecycleScope.launch { - if (serviceManager.autoTunnelActive.value) { - serviceManager.stopAutoTunnel() - setInactive() - } else { - serviceManager.startAutoTunnel() - setActive() - } - } - } - } + override fun onClick() { + super.onClick() + unlockAndRun { + lifecycleScope.launch { + if (serviceManager.autoTunnelActive.value) { + serviceManager.stopAutoTunnel() + setInactive() + } else { + serviceManager.startAutoTunnel() + setActive() + } + } + } + } - private fun setActive() { - runCatching { - qsTile.state = Tile.STATE_ACTIVE - qsTile.updateTile() - } - } + private fun setActive() { + runCatching { + qsTile.state = Tile.STATE_ACTIVE + qsTile.updateTile() + } + } - private fun setInactive() { - runCatching { - qsTile.state = Tile.STATE_INACTIVE - qsTile.updateTile() - } - } + private fun setInactive() { + runCatching { + qsTile.state = Tile.STATE_INACTIVE + qsTile.updateTile() + } + } - /* This works around an annoying unsolved frameworks bug some people are hitting. */ - override fun onBind(intent: Intent): IBinder? { - var ret: IBinder? = null - try { - ret = super.onBind(intent) - } catch (_: Throwable) { - Timber.e("Failed to bind to TunnelControlTile") - } - return ret - } + /* This works around an annoying unsolved frameworks bug some people are hitting. */ + override fun onBind(intent: Intent): IBinder? { + var ret: IBinder? = null + try { + ret = super.onBind(intent) + } catch (_: Throwable) { + Timber.e("Failed to bind to TunnelControlTile") + } + return ret + } - private fun setUnavailable() { - runCatching { - qsTile.state = Tile.STATE_UNAVAILABLE - qsTile.updateTile() - } - } + private fun setUnavailable() { + runCatching { + qsTile.state = Tile.STATE_UNAVAILABLE + qsTile.updateTile() + } + } - override val lifecycle: Lifecycle - get() = lifecycleRegistry + override val lifecycle: Lifecycle + get() = lifecycleRegistry } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/tile/TunnelControlTile.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/tile/TunnelControlTile.kt index e84fbaab..251be2cb 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/tile/TunnelControlTile.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/tile/TunnelControlTile.kt @@ -17,175 +17,167 @@ import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject @AndroidEntryPoint class TunnelControlTile : TileService(), LifecycleOwner { - @Inject - lateinit var appDataRepository: AppDataRepository + @Inject lateinit var appDataRepository: AppDataRepository - @Inject - lateinit var serviceManager: ServiceManager + @Inject lateinit var serviceManager: ServiceManager - @Inject - lateinit var tunnelManager: TunnelManager + @Inject lateinit var tunnelManager: TunnelManager - private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this) + private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this) - private var isCollecting = false + private var isCollecting = false - override fun onCreate() { - super.onCreate() - lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) - } + override fun onCreate() { + super.onCreate() + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) + } - override fun onDestroy() { - super.onDestroy() - lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) - } + override fun onDestroy() { + super.onDestroy() + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) + } - override fun onStartListening() { - super.onStartListening() - lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) - Timber.d("Start listening called for tunnel tile") - if (isCollecting) return - isCollecting = true - lifecycleScope.launch { - tunnelManager.activeTunnels.collect { - updateTileState() - } - } - } + override fun onStartListening() { + super.onStartListening() + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) + Timber.d("Start listening called for tunnel tile") + if (isCollecting) return + isCollecting = true + lifecycleScope.launch { tunnelManager.activeTunnels.collect { updateTileState() } } + } - private suspend fun updateTileState() { - try { - val tunnels = appDataRepository.tunnels.getAll() - if (tunnels.isEmpty()) { - setUnavailable() - return - } + private suspend fun updateTileState() { + try { + val tunnels = appDataRepository.tunnels.getAll() + if (tunnels.isEmpty()) { + setUnavailable() + return + } - val activeTunnels = tunnelManager.activeTunnels.value - .filter { it.value.status.isUpOrStarting() } + val activeTunnels = + tunnelManager.activeTunnels.value.filter { it.value.status.isUpOrStarting() } - when { - activeTunnels.isNotEmpty() -> { - val activeIds = activeTunnels.map { it.key.id } - // TODO improvements would be needed to make this work well with toggling multiple tunnels - // this would be better managed elsewhere - WireGuardAutoTunnel.setLastActiveTunnels(activeIds) - updateTileForActiveTunnels(activeTunnels) - } - else -> updateTileForLastActiveTunnels() - } - } catch (e: Exception) { - setUnavailable() - } - } + when { + activeTunnels.isNotEmpty() -> { + val activeIds = activeTunnels.map { it.key.id } + // TODO improvements would be needed to make this work well with toggling + // multiple tunnels + // this would be better managed elsewhere + WireGuardAutoTunnel.setLastActiveTunnels(activeIds) + updateTileForActiveTunnels(activeTunnels) + } + else -> updateTileForLastActiveTunnels() + } + } catch (e: Exception) { + setUnavailable() + } + } - private fun updateTileForActiveTunnels(activeTunnels: Map) { - val tileName = when (activeTunnels.size) { - 1 -> activeTunnels.keys.first().tunName - else -> getString(R.string.multiple) - } - updateTile(tileName, true) - } + private fun updateTileForActiveTunnels(activeTunnels: Map) { + val tileName = + when (activeTunnels.size) { + 1 -> activeTunnels.keys.first().tunName + else -> getString(R.string.multiple) + } + updateTile(tileName, true) + } - private suspend fun updateTileForLastActiveTunnels() { - val lastActiveIds = WireGuardAutoTunnel.getLastActiveTunnels() - when { - lastActiveIds.isEmpty() -> { - appDataRepository.getStartTunnelConfig()?.let { config -> - updateTile(config.tunName, false) - } ?: setUnavailable() - } - lastActiveIds.size > 1 -> updateTile(getString(R.string.multiple), false) - else -> { - val tunnelId = lastActiveIds.first() - appDataRepository.tunnels.getById(tunnelId)?.let { tunnel -> - updateTile(tunnel.tunName, false) - } ?: setUnavailable() - } - } - } + private suspend fun updateTileForLastActiveTunnels() { + val lastActiveIds = WireGuardAutoTunnel.getLastActiveTunnels() + when { + lastActiveIds.isEmpty() -> { + appDataRepository.getStartTunnelConfig()?.let { config -> + updateTile(config.tunName, false) + } ?: setUnavailable() + } + lastActiveIds.size > 1 -> updateTile(getString(R.string.multiple), false) + else -> { + val tunnelId = lastActiveIds.first() + appDataRepository.tunnels.getById(tunnelId)?.let { tunnel -> + updateTile(tunnel.tunName, false) + } ?: setUnavailable() + } + } + } - override fun onClick() { - super.onClick() - unlockAndRun { - lifecycleScope.launch { - if (tunnelManager.activeTunnels.value.isNotEmpty()) return@launch tunnelManager.stopTunnel() - val lastActive = WireGuardAutoTunnel.getLastActiveTunnels() - if (lastActive.isEmpty()) { - appDataRepository.getStartTunnelConfig()?.let { - tunnelManager.startTunnel(it) - } - } else { - lastActive.forEach { id -> - appDataRepository.tunnels.getById(id)?.let { - tunnelManager.startTunnel(it) - } - } - } - } - } - } + override fun onClick() { + super.onClick() + unlockAndRun { + lifecycleScope.launch { + if (tunnelManager.activeTunnels.value.isNotEmpty()) + return@launch tunnelManager.stopTunnel() + val lastActive = WireGuardAutoTunnel.getLastActiveTunnels() + if (lastActive.isEmpty()) { + appDataRepository.getStartTunnelConfig()?.let { tunnelManager.startTunnel(it) } + } else { + lastActive.forEach { id -> + appDataRepository.tunnels.getById(id)?.let { tunnelManager.startTunnel(it) } + } + } + } + } + } - private fun setActive() { - runCatching { - qsTile.state = Tile.STATE_ACTIVE - qsTile.updateTile() - } - } + private fun setActive() { + runCatching { + qsTile.state = Tile.STATE_ACTIVE + qsTile.updateTile() + } + } - private fun setInactive() { - runCatching { - qsTile.state = Tile.STATE_INACTIVE - qsTile.updateTile() - } - } + private fun setInactive() { + runCatching { + qsTile.state = Tile.STATE_INACTIVE + qsTile.updateTile() + } + } - private fun setUnavailable() { - runCatching { - qsTile.state = Tile.STATE_UNAVAILABLE - setTileDescription("") - qsTile.updateTile() - } - } + private fun setUnavailable() { + runCatching { + qsTile.state = Tile.STATE_UNAVAILABLE + setTileDescription("") + qsTile.updateTile() + } + } - private fun setTileDescription(description: String) { - runCatching { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - qsTile.subtitle = description - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - qsTile.stateDescription = description - } - qsTile.updateTile() - } - } + private fun setTileDescription(description: String) { + runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + qsTile.subtitle = description + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + qsTile.stateDescription = description + } + qsTile.updateTile() + } + } - /* This works around an annoying unsolved frameworks bug some people are hitting. */ - override fun onBind(intent: Intent): IBinder? { - var ret: IBinder? = null - try { - ret = super.onBind(intent) - } catch (_: Throwable) { - Timber.e("Failed to bind to TunnelControlTile") - } - return ret - } + /* This works around an annoying unsolved frameworks bug some people are hitting. */ + override fun onBind(intent: Intent): IBinder? { + var ret: IBinder? = null + try { + ret = super.onBind(intent) + } catch (_: Throwable) { + Timber.e("Failed to bind to TunnelControlTile") + } + return ret + } - private fun updateTile(name: String, active: Boolean) { - runCatching { - setTileDescription(name) - if (active) return setActive() - setInactive() - }.onFailure { - Timber.e(it) - } - } - override val lifecycle: Lifecycle - get() = lifecycleRegistry + private fun updateTile(name: String, active: Boolean) { + runCatching { + setTileDescription(name) + if (active) return setActive() + setInactive() + } + .onFailure { Timber.e(it) } + } + + override val lifecycle: Lifecycle + get() = lifecycleRegistry } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/shortcut/DynamicShortcutManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/shortcut/DynamicShortcutManager.kt index 538d08c1..2a92c35b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/shortcut/DynamicShortcutManager.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/shortcut/DynamicShortcutManager.kt @@ -10,70 +10,83 @@ import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext -class DynamicShortcutManager(private val context: Context, @IoDispatcher private val ioDispatcher: CoroutineDispatcher) : ShortcutManager { - override suspend fun addShortcuts() { - withContext(ioDispatcher) { - ShortcutManagerCompat.setDynamicShortcuts(context, createShortcuts()) - } - } +class DynamicShortcutManager( + private val context: Context, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, +) : ShortcutManager { + override suspend fun addShortcuts() { + withContext(ioDispatcher) { + ShortcutManagerCompat.setDynamicShortcuts(context, createShortcuts()) + } + } - override suspend fun removeShortcuts() { - withContext(ioDispatcher) { - ShortcutManagerCompat.removeDynamicShortcuts(context, createShortcuts().map { it.id }) - } - } + override suspend fun removeShortcuts() { + withContext(ioDispatcher) { + ShortcutManagerCompat.removeDynamicShortcuts(context, createShortcuts().map { it.id }) + } + } - private fun createShortcuts(): List { - return listOf( - buildShortcut( - context.getString(R.string.vpn_off), - context.getString(R.string.vpn_off), - context.getString(R.string.vpn_off), - intent = Intent(context, ShortcutsActivity::class.java).apply { - putExtra("className", "WireGuardTunnelService") - action = ShortcutsActivity.Action.STOP.name - }, - shortcutIcon = R.drawable.vpn_off, - ), - buildShortcut( - context.getString(R.string.vpn_on), - context.getString(R.string.vpn_on), - context.getString(R.string.vpn_on), - intent = Intent(context, ShortcutsActivity::class.java).apply { - putExtra("className", "WireGuardTunnelService") - action = ShortcutsActivity.Action.START.name - }, - shortcutIcon = R.drawable.vpn_on, - ), - buildShortcut( - context.getString(R.string.start_auto), - context.getString(R.string.start_auto), - context.getString(R.string.start_auto), - intent = Intent(context, ShortcutsActivity::class.java).apply { - putExtra("className", "WireGuardConnectivityWatcherService") - action = ShortcutsActivity.Action.START.name - }, - shortcutIcon = R.drawable.auto_play, - ), - buildShortcut( - context.getString(R.string.stop_auto), - context.getString(R.string.stop_auto), - context.getString(R.string.stop_auto), - intent = Intent(context, ShortcutsActivity::class.java).apply { - putExtra("className", "WireGuardConnectivityWatcherService") - action = ShortcutsActivity.Action.STOP.name - }, - shortcutIcon = R.drawable.auto_pause, - ), - ) - } + private fun createShortcuts(): List { + return listOf( + buildShortcut( + context.getString(R.string.vpn_off), + context.getString(R.string.vpn_off), + context.getString(R.string.vpn_off), + intent = + Intent(context, ShortcutsActivity::class.java).apply { + putExtra("className", "WireGuardTunnelService") + action = ShortcutsActivity.Action.STOP.name + }, + shortcutIcon = R.drawable.vpn_off, + ), + buildShortcut( + context.getString(R.string.vpn_on), + context.getString(R.string.vpn_on), + context.getString(R.string.vpn_on), + intent = + Intent(context, ShortcutsActivity::class.java).apply { + putExtra("className", "WireGuardTunnelService") + action = ShortcutsActivity.Action.START.name + }, + shortcutIcon = R.drawable.vpn_on, + ), + buildShortcut( + context.getString(R.string.start_auto), + context.getString(R.string.start_auto), + context.getString(R.string.start_auto), + intent = + Intent(context, ShortcutsActivity::class.java).apply { + putExtra("className", "WireGuardConnectivityWatcherService") + action = ShortcutsActivity.Action.START.name + }, + shortcutIcon = R.drawable.auto_play, + ), + buildShortcut( + context.getString(R.string.stop_auto), + context.getString(R.string.stop_auto), + context.getString(R.string.stop_auto), + intent = + Intent(context, ShortcutsActivity::class.java).apply { + putExtra("className", "WireGuardConnectivityWatcherService") + action = ShortcutsActivity.Action.STOP.name + }, + shortcutIcon = R.drawable.auto_pause, + ), + ) + } - private fun buildShortcut(id: String, shortLabel: String, longLabel: String, intent: Intent, shortcutIcon: Int): ShortcutInfoCompat { - return ShortcutInfoCompat.Builder(context, id) - .setShortLabel(shortLabel) - .setLongLabel(longLabel) - .setIntent(intent) - .setIcon(IconCompat.createWithResource(context, shortcutIcon)) - .build() - } + private fun buildShortcut( + id: String, + shortLabel: String, + longLabel: String, + intent: Intent, + shortcutIcon: Int, + ): ShortcutInfoCompat { + return ShortcutInfoCompat.Builder(context, id) + .setShortLabel(shortLabel) + .setLongLabel(longLabel) + .setIntent(intent) + .setIcon(IconCompat.createWithResource(context, shortcutIcon)) + .build() + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/shortcut/ShortcutManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/shortcut/ShortcutManager.kt index 465347c4..50c55ef6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/shortcut/ShortcutManager.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/shortcut/ShortcutManager.kt @@ -1,6 +1,7 @@ package com.zaneschepke.wireguardautotunnel.core.shortcut interface ShortcutManager { - suspend fun addShortcuts() - suspend fun removeShortcuts() + suspend fun addShortcuts() + + suspend fun removeShortcuts() } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/shortcut/ShortcutsActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/shortcut/ShortcutsActivity.kt index 0d0a8548..71c2c6bc 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/shortcut/ShortcutsActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/shortcut/ShortcutsActivity.kt @@ -9,69 +9,68 @@ import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject @AndroidEntryPoint class ShortcutsActivity : ComponentActivity() { - @Inject - lateinit var appDataRepository: AppDataRepository + @Inject lateinit var appDataRepository: AppDataRepository - @Inject - lateinit var serviceManager: ServiceManager + @Inject lateinit var serviceManager: ServiceManager - @Inject - lateinit var tunnelManager: TunnelManager + @Inject lateinit var tunnelManager: TunnelManager - @Inject - @ApplicationScope - lateinit var applicationScope: CoroutineScope + @Inject @ApplicationScope lateinit var applicationScope: CoroutineScope - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - applicationScope.launch { - val settings = appDataRepository.settings.get() - if (settings.isShortcutsEnabled) { - when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) { - LEGACY_TUNNEL_SERVICE_NAME, TunnelProvider::class.java.simpleName -> { - val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY) - Timber.d("Tunnel name extra: $tunnelName") - val tunnelConfig = tunnelName?.let { - appDataRepository.tunnels.getAll() - .firstOrNull { it.tunName == tunnelName } - } ?: appDataRepository.getStartTunnelConfig() - Timber.d("Shortcut action on name: ${tunnelConfig?.tunName}") - tunnelConfig?.let { - when (intent.action) { - Action.START.name -> tunnelManager.startTunnel(it) - Action.STOP.name -> tunnelManager.stopTunnel() - else -> Unit - } - } - } - AutoTunnelService::class.java.simpleName, LEGACY_AUTO_TUNNEL_SERVICE_NAME -> { - when (intent.action) { - Action.START.name -> serviceManager.startAutoTunnel() - Action.STOP.name -> serviceManager.stopAutoTunnel() - } - } - } - } - } - finish() - } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + applicationScope.launch { + val settings = appDataRepository.settings.get() + if (settings.isShortcutsEnabled) { + when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) { + LEGACY_TUNNEL_SERVICE_NAME, + TunnelProvider::class.java.simpleName -> { + val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY) + Timber.d("Tunnel name extra: $tunnelName") + val tunnelConfig = + tunnelName?.let { + appDataRepository.tunnels.getAll().firstOrNull { + it.tunName == tunnelName + } + } ?: appDataRepository.getStartTunnelConfig() + Timber.d("Shortcut action on name: ${tunnelConfig?.tunName}") + tunnelConfig?.let { + when (intent.action) { + Action.START.name -> tunnelManager.startTunnel(it) + Action.STOP.name -> tunnelManager.stopTunnel() + else -> Unit + } + } + } + AutoTunnelService::class.java.simpleName, + LEGACY_AUTO_TUNNEL_SERVICE_NAME -> { + when (intent.action) { + Action.START.name -> serviceManager.startAutoTunnel() + Action.STOP.name -> serviceManager.stopAutoTunnel() + } + } + } + } + } + finish() + } - enum class Action { - START, - STOP, - } + enum class Action { + START, + STOP, + } - companion object { - const val LEGACY_TUNNEL_SERVICE_NAME = "WireGuardTunnelService" - const val LEGACY_AUTO_TUNNEL_SERVICE_NAME = "WireGuardConnectivityWatcherService" - const val TUNNEL_NAME_EXTRA_KEY = "tunnelName" - const val CLASS_NAME_EXTRA_KEY = "className" - } + companion object { + const val LEGACY_TUNNEL_SERVICE_NAME = "WireGuardTunnelService" + const val LEGACY_AUTO_TUNNEL_SERVICE_NAME = "WireGuardConnectivityWatcherService" + const val TUNNEL_NAME_EXTRA_KEY = "tunnelName" + const val CLASS_NAME_EXTRA_KEY = "className" + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/BaseTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/BaseTunnel.kt index 31c29899..95337b04 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/BaseTunnel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/BaseTunnel.kt @@ -10,6 +10,9 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.concurrent.thread import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -19,185 +22,196 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import timber.log.Timber -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.atomic.AtomicBoolean -import kotlin.concurrent.thread abstract class BaseTunnel( - @ApplicationScope private val applicationScope: CoroutineScope, - private val appDataRepository: AppDataRepository, - private val serviceManager: ServiceManager, + @ApplicationScope private val applicationScope: CoroutineScope, + private val appDataRepository: AppDataRepository, + private val serviceManager: ServiceManager, ) : TunnelProvider { - private val activeTuns = MutableStateFlow>(emptyMap()) - private val tunThreads = ConcurrentHashMap() - override val activeTunnels = activeTuns.asStateFlow() + private val activeTuns = MutableStateFlow>(emptyMap()) + private val tunThreads = ConcurrentHashMap() + override val activeTunnels = activeTuns.asStateFlow() - private val tunMutex = Mutex() - private val tunStatusMutex = Mutex() + private val tunMutex = Mutex() + private val tunStatusMutex = Mutex() - private val isBouncing = AtomicBoolean(false) + private val isBouncing = AtomicBoolean(false) - abstract suspend fun startBackend(tunnel: TunnelConf) + abstract suspend fun startBackend(tunnel: TunnelConf) - abstract fun stopBackend(tunnel: TunnelConf) + abstract fun stopBackend(tunnel: TunnelConf) - override suspend fun clearError(tunnelConf: TunnelConf) = updateTunnelStatus(tunnelConf, TunnelStatus.Down) + override suspend fun clearError(tunnelConf: TunnelConf) = + updateTunnelStatus(tunnelConf, TunnelStatus.Down) - override fun hasVpnPermission(): Boolean { - return serviceManager.hasVpnPermission() - } + override fun hasVpnPermission(): Boolean { + return serviceManager.hasVpnPermission() + } - protected suspend fun updateTunnelStatus(tunnelConf: TunnelConf, state: TunnelStatus? = null, stats: TunnelStatistics? = null) { - tunStatusMutex.withLock { - activeTuns.update { current -> - val originalConf = current.getKeyById(tunnelConf.id) ?: tunnelConf - val existingState = current.getValueById(tunnelConf.id) ?: TunnelState() - val newState = state ?: existingState.status - if (newState == TunnelStatus.Down) { - Timber.d("Removing tunnel ${tunnelConf.id} from activeTunnels as state is DOWN") - cleanUpTunThread(tunnelConf) - current - originalConf - } else if (existingState.status == newState && stats == null) { - Timber.d("Skipping redundant state update for ${tunnelConf.id}: $newState") - current - } else { - val updated = existingState.copy( - status = newState, - statistics = stats ?: existingState.statistics, - ) - current + (originalConf to updated) - } - } - } - } + protected suspend fun updateTunnelStatus( + tunnelConf: TunnelConf, + state: TunnelStatus? = null, + stats: TunnelStatistics? = null, + ) { + tunStatusMutex.withLock { + activeTuns.update { current -> + val originalConf = current.getKeyById(tunnelConf.id) ?: tunnelConf + val existingState = current.getValueById(tunnelConf.id) ?: TunnelState() + val newState = state ?: existingState.status + if (newState == TunnelStatus.Down) { + Timber.d("Removing tunnel ${tunnelConf.id} from activeTunnels as state is DOWN") + cleanUpTunThread(tunnelConf) + current - originalConf + } else if (existingState.status == newState && stats == null) { + Timber.d("Skipping redundant state update for ${tunnelConf.id}: $newState") + current + } else { + val updated = + existingState.copy( + status = newState, + statistics = stats ?: existingState.statistics, + ) + current + (originalConf to updated) + } + } + } + } - private suspend fun stopActiveTunnels() { - activeTunnels.value.forEach { (config, state) -> - if (state.status.isUpOrStarting()) { - stopTunnel(config) - } - } - } + private suspend fun stopActiveTunnels() { + activeTunnels.value.forEach { (config, state) -> + if (state.status.isUpOrStarting()) { + stopTunnel(config) + } + } + } - private fun configureTunnelCallbacks(tunnelConf: TunnelConf) { - Timber.d("Configuring TunnelConf instance: ${tunnelConf.hashCode()}") - tunnelConf.setStateChangeCallback { state -> - applicationScope.launch { - Timber.d("State change callback triggered for tunnel ${tunnelConf.id}: ${tunnelConf.tunName} with state $state at ${System.currentTimeMillis()}") - when (state) { - is Tunnel.State -> updateTunnelStatus(tunnelConf, state.asTunnelState()) - is org.amnezia.awg.backend.Tunnel.State -> updateTunnelStatus(tunnelConf, state.asTunnelState()) - } - handleServiceChangesOnStop() - } - serviceManager.updateTunnelTile() - } - } + private fun configureTunnelCallbacks(tunnelConf: TunnelConf) { + Timber.d("Configuring TunnelConf instance: ${tunnelConf.hashCode()}") + tunnelConf.setStateChangeCallback { state -> + applicationScope.launch { + Timber.d( + "State change callback triggered for tunnel ${tunnelConf.id}: ${tunnelConf.tunName} with state $state at ${System.currentTimeMillis()}" + ) + when (state) { + is Tunnel.State -> updateTunnelStatus(tunnelConf, state.asTunnelState()) + is org.amnezia.awg.backend.Tunnel.State -> + updateTunnelStatus(tunnelConf, state.asTunnelState()) + } + handleServiceChangesOnStop() + } + serviceManager.updateTunnelTile() + } + } - override suspend fun updateTunnelStatistics(tunnel: TunnelConf) { - val stats = getStatistics(tunnel) - updateTunnelStatus(tunnel, null, stats) - } + override suspend fun updateTunnelStatistics(tunnel: TunnelConf) { + val stats = getStatistics(tunnel) + updateTunnelStatus(tunnel, null, stats) + } - override suspend fun startTunnel(tunnelConf: TunnelConf) { - if (activeTuns.exists(tunnelConf.id) || tunThreads.containsKey(tunnelConf.id)) return - // stop active tunnels if we are userspace - if (this@BaseTunnel is UserspaceTunnel) stopActiveTunnels() - tunMutex.withLock { - // use thread to interrupt java backend if stuck (like in dns resolution) - tunThreads += tunnelConf.id to thread { - runBlocking { - try { - Timber.d("Starting tunnel ${tunnelConf.id}...") - startTunnelInner(tunnelConf) - Timber.d("Started complete for tunnel ${tunnelConf.name}...") - } catch (e: BackendError) { - Timber.e(e, "Failed to start tunnel ${tunnelConf.name} userspace") - updateTunnelStatus(tunnelConf, TunnelStatus.Error(e)) - } catch (e: InterruptedException) { - Timber.i("Tunnel start has been interrupted as ${tunnelConf.name} failed to start") - } - } - } - } - } + override suspend fun startTunnel(tunnelConf: TunnelConf) { + if (activeTuns.exists(tunnelConf.id) || tunThreads.containsKey(tunnelConf.id)) return + // stop active tunnels if we are userspace + if (this@BaseTunnel is UserspaceTunnel) stopActiveTunnels() + tunMutex.withLock { + // use thread to interrupt java backend if stuck (like in dns resolution) + tunThreads += + tunnelConf.id to + thread { + runBlocking { + try { + Timber.d("Starting tunnel ${tunnelConf.id}...") + startTunnelInner(tunnelConf) + Timber.d("Started complete for tunnel ${tunnelConf.name}...") + } catch (e: BackendError) { + Timber.e(e, "Failed to start tunnel ${tunnelConf.name} userspace") + updateTunnelStatus(tunnelConf, TunnelStatus.Error(e)) + } catch (e: InterruptedException) { + Timber.i( + "Tunnel start has been interrupted as ${tunnelConf.name} failed to start" + ) + } + } + } + } + } - private suspend fun startTunnelInner(tunnelConf: TunnelConf) { - configureTunnelCallbacks(tunnelConf) - Timber.d("Started backend for tunnel ${tunnelConf.id}...") - startBackend(tunnelConf) - updateTunnelStatus(tunnelConf, TunnelStatus.Up) - Timber.d("DONE for tun ${tunnelConf.id}...") - saveTunnelActiveState(tunnelConf, true) - serviceManager.startTunnelForegroundService() - } + private suspend fun startTunnelInner(tunnelConf: TunnelConf) { + configureTunnelCallbacks(tunnelConf) + Timber.d("Started backend for tunnel ${tunnelConf.id}...") + startBackend(tunnelConf) + updateTunnelStatus(tunnelConf, TunnelStatus.Up) + Timber.d("DONE for tun ${tunnelConf.id}...") + saveTunnelActiveState(tunnelConf, true) + serviceManager.startTunnelForegroundService() + } - private suspend fun saveTunnelActiveState(tunnelConf: TunnelConf, active: Boolean) { - val tunnelCopy = tunnelConf.copyWithCallback(isActive = active) - appDataRepository.tunnels.save(tunnelCopy) - } + private suspend fun saveTunnelActiveState(tunnelConf: TunnelConf, active: Boolean) { + val tunnelCopy = tunnelConf.copyWithCallback(isActive = active) + appDataRepository.tunnels.save(tunnelCopy) + } - override suspend fun stopTunnel(tunnelConf: TunnelConf?, reason: TunnelStatus.StopReason) { - if (tunnelConf == null) return stopActiveTunnels() - tunMutex.withLock { - try { - if (activeTuns.isStarting(tunnelConf.id)) return handleStuckStartingTunnelShutdown(tunnelConf) - updateTunnelStatus(tunnelConf, TunnelStatus.Stopping(reason)) - stopTunnelInner(tunnelConf) - } catch (e: BackendError) { - Timber.e(e, "Failed to stop tunnel ${tunnelConf.id}") - updateTunnelStatus(tunnelConf, TunnelStatus.Error(e)) - } - } - } + override suspend fun stopTunnel(tunnelConf: TunnelConf?, reason: TunnelStatus.StopReason) { + if (tunnelConf == null) return stopActiveTunnels() + tunMutex.withLock { + try { + if (activeTuns.isStarting(tunnelConf.id)) + return handleStuckStartingTunnelShutdown(tunnelConf) + updateTunnelStatus(tunnelConf, TunnelStatus.Stopping(reason)) + stopTunnelInner(tunnelConf) + } catch (e: BackendError) { + Timber.e(e, "Failed to stop tunnel ${tunnelConf.id}") + updateTunnelStatus(tunnelConf, TunnelStatus.Error(e)) + } + } + } - private suspend fun stopTunnelInner(tunnelConf: TunnelConf) { - val tunnel = activeTuns.findTunnel(tunnelConf.id) ?: return - stopBackend(tunnel) - saveTunnelActiveState(tunnelConf, false) - removeActiveTunnel(tunnel) - } + private suspend fun stopTunnelInner(tunnelConf: TunnelConf) { + val tunnel = activeTuns.findTunnel(tunnelConf.id) ?: return + stopBackend(tunnel) + saveTunnelActiveState(tunnelConf, false) + removeActiveTunnel(tunnel) + } - private suspend fun handleServiceChangesOnStop() { - if (activeTuns.value.isEmpty() && !isBouncing.get()) return serviceManager.stopTunnelForegroundService() - } + private suspend fun handleServiceChangesOnStop() { + if (activeTuns.value.isEmpty() && !isBouncing.get()) + return serviceManager.stopTunnelForegroundService() + } - private suspend fun handleStuckStartingTunnelShutdown(tunnel: TunnelConf) { - Timber.d("Stuck in starting state so shutting down tunnel thread for tunnel ${tunnel.name}") - try { - tunThreads[tunnel.id]?.let { - if (it.state != Thread.State.TERMINATED) { - it.interrupt() - updateTunnelStatus(tunnel, TunnelStatus.Down) - } else { - Timber.d("Thread already terminated") - } - } - } catch (e: Exception) { - Timber.e(e, "Failed to stop tunnel thread for ${tunnel.name}") - } - cleanUpTunThread(tunnel) - } + private suspend fun handleStuckStartingTunnelShutdown(tunnel: TunnelConf) { + Timber.d("Stuck in starting state so shutting down tunnel thread for tunnel ${tunnel.name}") + try { + tunThreads[tunnel.id]?.let { + if (it.state != Thread.State.TERMINATED) { + it.interrupt() + updateTunnelStatus(tunnel, TunnelStatus.Down) + } else { + Timber.d("Thread already terminated") + } + } + } catch (e: Exception) { + Timber.e(e, "Failed to stop tunnel thread for ${tunnel.name}") + } + cleanUpTunThread(tunnel) + } - private fun cleanUpTunThread(tunnel: TunnelConf) { - Timber.d("Removing thread for ${tunnel.name}") - tunThreads -= tunnel.id - } + private fun cleanUpTunThread(tunnel: TunnelConf) { + Timber.d("Removing thread for ${tunnel.name}") + tunThreads -= tunnel.id + } - private fun removeActiveTunnel(tunnelConf: TunnelConf) { - activeTuns.update { current -> - current.toMutableMap().apply { remove(tunnelConf) } - } - } + private fun removeActiveTunnel(tunnelConf: TunnelConf) { + activeTuns.update { current -> current.toMutableMap().apply { remove(tunnelConf) } } + } - override suspend fun bounceTunnel(tunnelConf: TunnelConf, reason: TunnelStatus.StopReason) { - Timber.i("Bounce tunnel ${tunnelConf.name}") - isBouncing.set(true) - stopTunnel(tunnelConf, reason) - startTunnel(tunnelConf) - isBouncing.set(false) - } + override suspend fun bounceTunnel(tunnelConf: TunnelConf, reason: TunnelStatus.StopReason) { + Timber.i("Bounce tunnel ${tunnelConf.name}") + isBouncing.set(true) + stopTunnel(tunnelConf, reason) + startTunnel(tunnelConf) + isBouncing.set(false) + } - override suspend fun runningTunnelNames(): Set = activeTuns.value.keys.map { it.tunName }.toSet() + override suspend fun runningTunnelNames(): Set = + activeTuns.value.keys.map { it.tunName }.toSet() } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/Extensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/Extensions.kt index 623f8bad..cd9ce280 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/Extensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/Extensions.kt @@ -6,46 +6,45 @@ import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState import kotlinx.coroutines.flow.MutableStateFlow fun Map.allDown(): Boolean { - return this.all { it.value.status.isDown() } + return this.all { it.value.status.isDown() } } fun Map.hasActive(): Boolean { - return this.any { it.value.status.isUp() } + return this.any { it.value.status.isUp() } } fun Map.getValueById(id: Int): TunnelState? { - val key = this.keys.find { it.id == id } - return key?.let { this@getValueById[it] } + val key = this.keys.find { it.id == id } + return key?.let { this@getValueById[it] } } fun Map.getKeyById(id: Int): TunnelConf? { - return this.keys.find { it.id == id } + return this.keys.find { it.id == id } } fun Map.isUp(tunnelConf: TunnelConf): Boolean { - return this.getValueById(tunnelConf.id)?.status?.isUp() ?: false + return this.getValueById(tunnelConf.id)?.status?.isUp() ?: false } fun MutableStateFlow>.exists(id: Int): Boolean { - return this.value.any { it.key.id == id } + return this.value.any { it.key.id == id } } fun MutableStateFlow>.isUp(id: Int): Boolean { - return this.value.any { it.key.id == id && it.value.status == TunnelStatus.Up } + return this.value.any { it.key.id == id && it.value.status == TunnelStatus.Up } } fun MutableStateFlow>.isStarting(id: Int): Boolean { - return this.value.any { it.key.id == id && it.value.status == TunnelStatus.Starting } + return this.value.any { it.key.id == id && it.value.status == TunnelStatus.Starting } } fun MutableStateFlow>.findTunnel(id: Int): TunnelConf? { - return this.value.keys.find { it.id == id } + return this.value.keys.find { it.id == id } } -private val URL_PATTERN = Regex( - """^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}:[0-9]{1,5}$""", -) +private val URL_PATTERN = + Regex("""^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}:[0-9]{1,5}$""") fun String.isUrl(): Boolean { - return URL_PATTERN.matches(this) + return URL_PATTERN.matches(this) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/KernelTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/KernelTunnel.kt index 0e478421..af7fefcb 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/KernelTunnel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/KernelTunnel.kt @@ -12,53 +12,55 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError +import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import timber.log.Timber -import javax.inject.Inject -class KernelTunnel @Inject constructor( - @ApplicationScope private val applicationScope: CoroutineScope, - serviceManager: ServiceManager, - appDataRepository: AppDataRepository, - private val backend: Backend, +class KernelTunnel +@Inject +constructor( + @ApplicationScope private val applicationScope: CoroutineScope, + serviceManager: ServiceManager, + appDataRepository: AppDataRepository, + private val backend: Backend, ) : BaseTunnel(applicationScope, appDataRepository, serviceManager) { - override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? { - return try { - WireGuardStatistics(backend.getStatistics(tunnelConf)) - } catch (e: Exception) { - Timber.e(e) - null - } - } + override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? { + return try { + WireGuardStatistics(backend.getStatistics(tunnelConf)) + } catch (e: Exception) { + Timber.e(e) + null + } + } - override suspend fun startBackend(tunnel: TunnelConf) { - try { - updateTunnelStatus(tunnel, TunnelStatus.Starting) - backend.setState(tunnel, Tunnel.State.UP, tunnel.toWgConfig()) - } catch (e: BackendException) { - throw e.toBackendError() - } - } + override suspend fun startBackend(tunnel: TunnelConf) { + try { + updateTunnelStatus(tunnel, TunnelStatus.Starting) + backend.setState(tunnel, Tunnel.State.UP, tunnel.toWgConfig()) + } catch (e: BackendException) { + throw e.toBackendError() + } + } - override fun stopBackend(tunnel: TunnelConf) { - Timber.i("Stopping tunnel ${tunnel.id} kernel") - try { - backend.setState(tunnel, Tunnel.State.DOWN, tunnel.toWgConfig()) - } catch (e: BackendException) { - throw e.toBackendError() - } - } + override fun stopBackend(tunnel: TunnelConf) { + Timber.i("Stopping tunnel ${tunnel.id} kernel") + try { + backend.setState(tunnel, Tunnel.State.DOWN, tunnel.toWgConfig()) + } catch (e: BackendException) { + throw e.toBackendError() + } + } - override fun setBackendState(backendState: BackendState, allowedIps: Collection) { - Timber.w("Not yet implemented for kernel") - } + override fun setBackendState(backendState: BackendState, allowedIps: Collection) { + Timber.w("Not yet implemented for kernel") + } - override fun getBackendState(): BackendState { - return BackendState.INACTIVE - } + override fun getBackendState(): BackendState { + return BackendState.INACTIVE + } - override suspend fun runningTunnelNames(): Set { - return backend.runningTunnelNames - } + override suspend fun runningTunnelNames(): Set { + return backend.runningTunnelNames + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/TunnelManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/TunnelManager.kt index 6e65b864..28795d5b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/TunnelManager.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/TunnelManager.kt @@ -9,6 +9,7 @@ import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics +import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -19,96 +20,101 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.plus -import javax.inject.Inject -class TunnelManager @Inject constructor( - @Kernel private val kernelTunnel: TunnelProvider, - @Userspace private val userspaceTunnel: TunnelProvider, - private val appDataRepository: AppDataRepository, - @ApplicationScope private val applicationScope: CoroutineScope, - @IoDispatcher private val ioDispatcher: CoroutineDispatcher, +class TunnelManager +@Inject +constructor( + @Kernel private val kernelTunnel: TunnelProvider, + @Userspace private val userspaceTunnel: TunnelProvider, + private val appDataRepository: AppDataRepository, + @ApplicationScope private val applicationScope: CoroutineScope, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, ) : TunnelProvider { - @OptIn(ExperimentalCoroutinesApi::class) - private val tunnelProviderFlow = appDataRepository.settings.flow - .filterNotNull() - .flatMapLatest { settings -> - MutableStateFlow(if (settings.isKernelEnabled) kernelTunnel else userspaceTunnel) - } - .stateIn( - scope = applicationScope.plus(ioDispatcher), - started = SharingStarted.Eagerly, - initialValue = userspaceTunnel, - ) + @OptIn(ExperimentalCoroutinesApi::class) + private val tunnelProviderFlow = + appDataRepository.settings.flow + .filterNotNull() + .flatMapLatest { settings -> + MutableStateFlow(if (settings.isKernelEnabled) kernelTunnel else userspaceTunnel) + } + .stateIn( + scope = applicationScope.plus(ioDispatcher), + started = SharingStarted.Eagerly, + initialValue = userspaceTunnel, + ) - @OptIn(ExperimentalCoroutinesApi::class) - override val activeTunnels = appDataRepository.settings.flow - .filterNotNull() - .flatMapLatest { settings -> - if (settings.isKernelEnabled) { - kernelTunnel.activeTunnels - } else { - userspaceTunnel.activeTunnels - } - } - .stateIn( - scope = applicationScope.plus(ioDispatcher), - started = SharingStarted.Eagerly, - initialValue = emptyMap(), - ) + @OptIn(ExperimentalCoroutinesApi::class) + override val activeTunnels = + appDataRepository.settings.flow + .filterNotNull() + .flatMapLatest { settings -> + if (settings.isKernelEnabled) { + kernelTunnel.activeTunnels + } else { + userspaceTunnel.activeTunnels + } + } + .stateIn( + scope = applicationScope.plus(ioDispatcher), + started = SharingStarted.Eagerly, + initialValue = emptyMap(), + ) - override fun hasVpnPermission(): Boolean { - return userspaceTunnel.hasVpnPermission() - } + override fun hasVpnPermission(): Boolean { + return userspaceTunnel.hasVpnPermission() + } - override suspend fun clearError(tunnelConf: TunnelConf) { - tunnelProviderFlow.value.clearError(tunnelConf) - } + override suspend fun clearError(tunnelConf: TunnelConf) { + tunnelProviderFlow.value.clearError(tunnelConf) + } - override suspend fun updateTunnelStatistics(tunnel: TunnelConf) { - tunnelProviderFlow.value.updateTunnelStatistics(tunnel) - } + override suspend fun updateTunnelStatistics(tunnel: TunnelConf) { + tunnelProviderFlow.value.updateTunnelStatistics(tunnel) + } - override suspend fun startTunnel(tunnelConf: TunnelConf) { - tunnelProviderFlow.value.startTunnel(tunnelConf) - } + override suspend fun startTunnel(tunnelConf: TunnelConf) { + tunnelProviderFlow.value.startTunnel(tunnelConf) + } - override suspend fun stopTunnel(tunnelConf: TunnelConf?, reason: TunnelStatus.StopReason) { - tunnelProviderFlow.value.stopTunnel(tunnelConf) - } + override suspend fun stopTunnel(tunnelConf: TunnelConf?, reason: TunnelStatus.StopReason) { + tunnelProviderFlow.value.stopTunnel(tunnelConf) + } - override suspend fun bounceTunnel(tunnelConf: TunnelConf, reason: TunnelStatus.StopReason) { - tunnelProviderFlow.value.bounceTunnel(tunnelConf) - } + override suspend fun bounceTunnel(tunnelConf: TunnelConf, reason: TunnelStatus.StopReason) { + tunnelProviderFlow.value.bounceTunnel(tunnelConf) + } - override fun setBackendState(backendState: BackendState, allowedIps: Collection) { - tunnelProviderFlow.value.setBackendState(backendState, allowedIps) - } + override fun setBackendState(backendState: BackendState, allowedIps: Collection) { + tunnelProviderFlow.value.setBackendState(backendState, allowedIps) + } - override fun getBackendState(): BackendState { - return tunnelProviderFlow.value.getBackendState() - } + override fun getBackendState(): BackendState { + return tunnelProviderFlow.value.getBackendState() + } - override suspend fun runningTunnelNames(): Set { - return tunnelProviderFlow.value.runningTunnelNames() - } + override suspend fun runningTunnelNames(): Set { + return tunnelProviderFlow.value.runningTunnelNames() + } - override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? { - return tunnelProviderFlow.value.getStatistics(tunnelConf) - } + override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? { + return tunnelProviderFlow.value.getStatistics(tunnelConf) + } - fun restorePreviousState() = applicationScope.launch(ioDispatcher) { - val settings = appDataRepository.settings.get() - if (settings.isRestoreOnBootEnabled) { - val previouslyActiveTuns = appDataRepository.tunnels.getActive() - val tunsToStart = previouslyActiveTuns.filterNot { tun -> activeTunnels.value.any { tun.id == it.key.id } } - if (settings.isKernelEnabled) { - return@launch tunsToStart.forEach { - startTunnel(it) - } - } else { - tunsToStart.firstOrNull()?.let { startTunnel(it) } - } - } - } + fun restorePreviousState() = + applicationScope.launch(ioDispatcher) { + val settings = appDataRepository.settings.get() + if (settings.isRestoreOnBootEnabled) { + val previouslyActiveTuns = appDataRepository.tunnels.getActive() + val tunsToStart = + previouslyActiveTuns.filterNot { tun -> + activeTunnels.value.any { tun.id == it.key.id } + } + if (settings.isKernelEnabled) { + return@launch tunsToStart.forEach { startTunnel(it) } + } else { + tunsToStart.firstOrNull()?.let { startTunnel(it) } + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/TunnelProvider.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/TunnelProvider.kt index cec1527d..e2600dc6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/TunnelProvider.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/TunnelProvider.kt @@ -8,15 +8,31 @@ import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics import kotlinx.coroutines.flow.StateFlow interface TunnelProvider { - suspend fun startTunnel(tunnelConf: TunnelConf) - suspend fun stopTunnel(tunnelConf: TunnelConf? = null, reason: TunnelStatus.StopReason = TunnelStatus.StopReason.USER) - suspend fun bounceTunnel(tunnelConf: TunnelConf, reason: TunnelStatus.StopReason = TunnelStatus.StopReason.USER) - fun setBackendState(backendState: BackendState, allowedIps: Collection) - fun getBackendState(): BackendState - suspend fun runningTunnelNames(): Set - fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? - val activeTunnels: StateFlow> - fun hasVpnPermission(): Boolean - suspend fun clearError(tunnelConf: TunnelConf) - suspend fun updateTunnelStatistics(tunnel: TunnelConf) + suspend fun startTunnel(tunnelConf: TunnelConf) + + suspend fun stopTunnel( + tunnelConf: TunnelConf? = null, + reason: TunnelStatus.StopReason = TunnelStatus.StopReason.USER, + ) + + suspend fun bounceTunnel( + tunnelConf: TunnelConf, + reason: TunnelStatus.StopReason = TunnelStatus.StopReason.USER, + ) + + fun setBackendState(backendState: BackendState, allowedIps: Collection) + + fun getBackendState(): BackendState + + suspend fun runningTunnelNames(): Set + + fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? + + val activeTunnels: StateFlow> + + fun hasVpnPermission(): Boolean + + suspend fun clearError(tunnelConf: TunnelConf) + + suspend fun updateTunnelStatistics(tunnel: TunnelConf) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/UserspaceTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/UserspaceTunnel.kt index 5c09762f..adf9c28a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/UserspaceTunnel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/UserspaceTunnel.kt @@ -11,94 +11,97 @@ import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendState import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError +import javax.inject.Inject +import kotlin.jvm.optionals.getOrNull import kotlinx.coroutines.CoroutineScope import org.amnezia.awg.backend.Backend import org.amnezia.awg.backend.BackendException import org.amnezia.awg.backend.Tunnel import org.amnezia.awg.config.Config import timber.log.Timber -import javax.inject.Inject -import kotlin.jvm.optionals.getOrNull -class UserspaceTunnel @Inject constructor( - @ApplicationScope private val applicationScope: CoroutineScope, - val serviceManager: ServiceManager, - val appDataRepository: AppDataRepository, - private val backend: Backend, +class UserspaceTunnel +@Inject +constructor( + @ApplicationScope private val applicationScope: CoroutineScope, + val serviceManager: ServiceManager, + val appDataRepository: AppDataRepository, + private val backend: Backend, ) : BaseTunnel(applicationScope, appDataRepository, serviceManager) { - private var previousBackendState: Pair? = null + private var previousBackendState: Pair? = null - override suspend fun startBackend(tunnel: TunnelConf) { - try { - updateTunnelStatus(tunnel, TunnelStatus.Starting) - val amConfig = tunnel.toAmConfig() - handleVpnKillSwitchWithDomainEndpoints(amConfig) - backend.setState(tunnel, Tunnel.State.UP, amConfig) - } catch (e: BackendException) { - Timber.e(e, "Failed to start up backend for tunnel ${tunnel.name}") - throw e.toBackendError() - } - } + override suspend fun startBackend(tunnel: TunnelConf) { + try { + updateTunnelStatus(tunnel, TunnelStatus.Starting) + val amConfig = tunnel.toAmConfig() + handleVpnKillSwitchWithDomainEndpoints(amConfig) + backend.setState(tunnel, Tunnel.State.UP, amConfig) + } catch (e: BackendException) { + Timber.e(e, "Failed to start up backend for tunnel ${tunnel.name}") + throw e.toBackendError() + } + } - override fun stopBackend(tunnel: TunnelConf) { - Timber.i("Stopping tunnel ${tunnel.name} userspace") - try { - backend.setState(tunnel, Tunnel.State.DOWN, tunnel.toAmConfig()) - } catch (e: BackendException) { - Timber.e(e, "Failed to stop tunnel ${tunnel.id}") - throw e.toBackendError() - } - handlePreviouslyEnabledVpnKillSwitch() - } + override fun stopBackend(tunnel: TunnelConf) { + Timber.i("Stopping tunnel ${tunnel.name} userspace") + try { + backend.setState(tunnel, Tunnel.State.DOWN, tunnel.toAmConfig()) + } catch (e: BackendException) { + Timber.e(e, "Failed to stop tunnel ${tunnel.id}") + throw e.toBackendError() + } + handlePreviouslyEnabledVpnKillSwitch() + } - // stop vpn kill switch if we need to resolve DNS for peer endpoints - private suspend fun handleVpnKillSwitchWithDomainEndpoints(config: Config) { - if (config.peers.any { it.endpoint.getOrNull()?.toString()?.isUrl() == true } && - backend.backendState.asBackendState() == BackendState.KILL_SWITCH_ACTIVE - ) { - val bypassLan = appDataRepository.settings.get().isLanOnKillSwitchEnabled - previousBackendState = Pair(BackendState.KILL_SWITCH_ACTIVE, bypassLan) - setBackendState(BackendState.SERVICE_ACTIVE, emptyList()) - } - } + // stop vpn kill switch if we need to resolve DNS for peer endpoints + private suspend fun handleVpnKillSwitchWithDomainEndpoints(config: Config) { + if ( + config.peers.any { it.endpoint.getOrNull()?.toString()?.isUrl() == true } && + backend.backendState.asBackendState() == BackendState.KILL_SWITCH_ACTIVE + ) { + val bypassLan = appDataRepository.settings.get().isLanOnKillSwitchEnabled + previousBackendState = Pair(BackendState.KILL_SWITCH_ACTIVE, bypassLan) + setBackendState(BackendState.SERVICE_ACTIVE, emptyList()) + } + } - // restore vpn kill switch if needed - private fun handlePreviouslyEnabledVpnKillSwitch() { - // let auto tunnel handle this if it is active - if (!serviceManager.autoTunnelActive.value) { - previousBackendState?.let { (state, lanEnabled) -> - Timber.d("Restoring kill switch configuration") - val lan = if (lanEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS else emptyList() - backend.setBackendState(state.asAmBackendState(), lan) - } - } - previousBackendState = null - } + // restore vpn kill switch if needed + private fun handlePreviouslyEnabledVpnKillSwitch() { + // let auto tunnel handle this if it is active + if (!serviceManager.autoTunnelActive.value) { + previousBackendState?.let { (state, lanEnabled) -> + Timber.d("Restoring kill switch configuration") + val lan = if (lanEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS else emptyList() + backend.setBackendState(state.asAmBackendState(), lan) + } + } + previousBackendState = null + } - override fun setBackendState(backendState: BackendState, allowedIps: Collection) { - Timber.d("Setting backend state: $backendState with allowedIps: $allowedIps") - try { - backend.setBackendState(backendState.asAmBackendState(), allowedIps) - } catch (e: BackendException) { - throw e.toBackendError() - } - } + override fun setBackendState(backendState: BackendState, allowedIps: Collection) { + Timber.d("Setting backend state: $backendState with allowedIps: $allowedIps") + try { + backend.setBackendState(backendState.asAmBackendState(), allowedIps) + } catch (e: BackendException) { + throw e.toBackendError() + } + } - override fun getBackendState(): BackendState { - return backend.backendState.asBackendState() - } + override fun getBackendState(): BackendState { + return backend.backendState.asBackendState() + } - override suspend fun runningTunnelNames(): Set { - return backend.runningTunnelNames - } + override suspend fun runningTunnelNames(): Set { + return backend.runningTunnelNames + } - override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? { - return try { - AmneziaStatistics(backend.getStatistics(tunnelConf)) - } catch (e: Exception) { - Timber.e(e, "Failed to get stats for ${tunnelConf.tunName}") - null - } - } + override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? { + return try { + AmneziaStatistics(backend.getStatistics(tunnelConf)) + } catch (e: Exception) { + Timber.e(e, "Failed to get stats for ${tunnelConf.tunName}") + null + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/worker/ServiceWorker.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/worker/ServiceWorker.kt index d30db9f0..07095b2a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/worker/ServiceWorker.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/worker/ServiceWorker.kt @@ -13,48 +13,55 @@ import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository import dagger.assisted.Assisted import dagger.assisted.AssistedInject +import java.util.concurrent.TimeUnit import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext import timber.log.Timber -import java.util.concurrent.TimeUnit @HiltWorker -class ServiceWorker @AssistedInject constructor( - @Assisted private val context: Context, - @Assisted private val params: WorkerParameters, - private val serviceManager: ServiceManager, - private val appDataRepository: AppDataRepository, - @IoDispatcher private val ioDispatcher: CoroutineDispatcher, - private val tunnelManager: TunnelManager, +class ServiceWorker +@AssistedInject +constructor( + @Assisted private val context: Context, + @Assisted private val params: WorkerParameters, + private val serviceManager: ServiceManager, + private val appDataRepository: AppDataRepository, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + private val tunnelManager: TunnelManager, ) : CoroutineWorker(context, params) { - companion object { - private const val TAG = "service_worker" + companion object { + private const val TAG = "service_worker" - fun stop(context: Context) { - WorkManager.getInstance(context).cancelAllWorkByTag(TAG) - } + fun stop(context: Context) { + WorkManager.getInstance(context).cancelAllWorkByTag(TAG) + } - fun start(context: Context) { - val periodicWorkRequest = PeriodicWorkRequestBuilder( - repeatInterval = 15, - repeatIntervalTimeUnit = TimeUnit.MINUTES, - ).build() - WorkManager.getInstance(context) - .enqueueUniquePeriodicWork( - TAG, - ExistingPeriodicWorkPolicy.KEEP, - periodicWorkRequest, - ) - } - } + fun start(context: Context) { + val periodicWorkRequest = + PeriodicWorkRequestBuilder( + repeatInterval = 15, + repeatIntervalTimeUnit = TimeUnit.MINUTES, + ) + .build() + WorkManager.getInstance(context) + .enqueueUniquePeriodicWork( + TAG, + ExistingPeriodicWorkPolicy.KEEP, + periodicWorkRequest, + ) + } + } - override suspend fun doWork(): Result = withContext(ioDispatcher) { - Timber.i("Service worker started") - with(appDataRepository.settings.get()) { - if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@with serviceManager.startAutoTunnel() - if (tunnelManager.activeTunnels.value.isEmpty()) tunnelManager.restorePreviousState() - } - Result.success() - } + override suspend fun doWork(): Result = + withContext(ioDispatcher) { + Timber.i("Service worker started") + with(appDataRepository.settings.get()) { + if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) + return@with serviceManager.startAutoTunnel() + if (tunnelManager.activeTunnels.value.isEmpty()) + tunnelManager.restorePreviousState() + } + Result.success() + } } 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 ec8b2c92..53851034 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt @@ -12,79 +12,38 @@ import com.zaneschepke.wireguardautotunnel.data.model.Settings import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig @Database( - entities = [Settings::class, TunnelConfig::class], - version = 16, - autoMigrations = - [ - AutoMigration(from = 1, to = 2), - AutoMigration(from = 2, to = 3), - AutoMigration( - from = 3, - to = 4, - ), - AutoMigration( - from = 4, - to = 5, - ), - AutoMigration( - from = 5, - to = 6, - ), - AutoMigration( - from = 6, - to = 7, - spec = RemoveLegacySettingColumnsMigration::class, - ), - AutoMigration(7, 8), - AutoMigration(8, 9), - AutoMigration(9, 10), - AutoMigration( - from = 10, - to = 11, - spec = RemoveTunnelPauseMigration::class, - ), - AutoMigration( - from = 11, - to = 12, - ), - AutoMigration( - from = 12, - to = 13, - ), - AutoMigration( - from = 13, - to = 14, - ), - AutoMigration( - from = 14, - to = 15, - ), - AutoMigration( - from = 15, - to = 16, - ), - ], - exportSchema = true, + entities = [Settings::class, TunnelConfig::class], + version = 16, + autoMigrations = + [ + AutoMigration(from = 1, to = 2), + AutoMigration(from = 2, to = 3), + AutoMigration(from = 3, to = 4), + AutoMigration(from = 4, to = 5), + AutoMigration(from = 5, to = 6), + AutoMigration(from = 6, to = 7, spec = RemoveLegacySettingColumnsMigration::class), + AutoMigration(7, 8), + AutoMigration(8, 9), + AutoMigration(9, 10), + AutoMigration(from = 10, to = 11, spec = RemoveTunnelPauseMigration::class), + AutoMigration(from = 11, to = 12), + AutoMigration(from = 12, to = 13), + AutoMigration(from = 13, to = 14), + AutoMigration(from = 14, to = 15), + AutoMigration(from = 15, to = 16), + ], + exportSchema = true, ) @TypeConverters(DatabaseListConverters::class) abstract class AppDatabase : RoomDatabase() { - abstract fun settingDao(): SettingsDao + abstract fun settingDao(): SettingsDao - abstract fun tunnelConfigDoa(): TunnelConfigDao + abstract fun tunnelConfigDoa(): TunnelConfigDao } -@DeleteColumn( - tableName = "Settings", - columnName = "default_tunnel", -) -@DeleteColumn( - tableName = "Settings", - columnName = "is_battery_saver_enabled", -) +@DeleteColumn(tableName = "Settings", columnName = "default_tunnel") +@DeleteColumn(tableName = "Settings", columnName = "is_battery_saver_enabled") class RemoveLegacySettingColumnsMigration : AutoMigrationSpec -@DeleteColumn( - tableName = "Settings", - columnName = "is_auto_tunnel_paused", -) +@DeleteColumn(tableName = "Settings", columnName = "is_auto_tunnel_paused") class RemoveTunnelPauseMigration : AutoMigrationSpec diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/DataStoreManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/DataStoreManager.kt index d7589306..fcf166f1 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/DataStoreManager.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/DataStoreManager.kt @@ -7,6 +7,7 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import com.zaneschepke.wireguardautotunnel.di.IoDispatcher +import java.io.IOException import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first @@ -15,82 +16,78 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import timber.log.Timber -import java.io.IOException class DataStoreManager( - private val context: Context, - @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + private val context: Context, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, ) { - companion object { - val locationDisclosureShown = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN") - val batteryDisableShown = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN") - val currentSSID = stringPreferencesKey("CURRENT_SSID") - val pinLockEnabled = booleanPreferencesKey("PIN_LOCK_ENABLED") - val tunnelStatsExpanded = booleanPreferencesKey("TUNNEL_STATS_EXPANDED") - val isLocalLogsEnabled = booleanPreferencesKey("LOCAL_LOGS_ENABLED") - val locale = stringPreferencesKey("LOCALE") - val theme = stringPreferencesKey("THEME") - val isRemoteControlEnabled = booleanPreferencesKey("IS_REMOTE_CONTROL_ENABLED") - val remoteKey = stringPreferencesKey("REMOTE_KEY") - } + companion object { + val locationDisclosureShown = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN") + val batteryDisableShown = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN") + val currentSSID = stringPreferencesKey("CURRENT_SSID") + val pinLockEnabled = booleanPreferencesKey("PIN_LOCK_ENABLED") + val tunnelStatsExpanded = booleanPreferencesKey("TUNNEL_STATS_EXPANDED") + val isLocalLogsEnabled = booleanPreferencesKey("LOCAL_LOGS_ENABLED") + val locale = stringPreferencesKey("LOCALE") + val theme = stringPreferencesKey("THEME") + val isRemoteControlEnabled = booleanPreferencesKey("IS_REMOTE_CONTROL_ENABLED") + val remoteKey = stringPreferencesKey("REMOTE_KEY") + } - // preferences - private val preferencesKey = "preferences" - private val Context.dataStore by - preferencesDataStore( - name = preferencesKey, - ) + // preferences + private val preferencesKey = "preferences" + private val Context.dataStore by preferencesDataStore(name = preferencesKey) - suspend fun init() { - withContext(ioDispatcher) { - try { - context.dataStore.data.first() - } catch (e: IOException) { - Timber.Forest.e(e) - } - } - } + suspend fun init() { + withContext(ioDispatcher) { + try { + context.dataStore.data.first() + } catch (e: IOException) { + Timber.Forest.e(e) + } + } + } - suspend fun saveToDataStore(key: Preferences.Key, value: T) { - withContext(ioDispatcher) { - try { - context.dataStore.edit { it[key] = value } - } catch (e: IOException) { - Timber.Forest.e(e) - } catch (e: Exception) { - Timber.Forest.e(e) - } - } - } + suspend fun saveToDataStore(key: Preferences.Key, value: T) { + withContext(ioDispatcher) { + try { + context.dataStore.edit { it[key] = value } + } catch (e: IOException) { + Timber.Forest.e(e) + } catch (e: Exception) { + Timber.Forest.e(e) + } + } + } - suspend fun removeFromDataStore(key: Preferences.Key) { - withContext(ioDispatcher) { - try { - context.dataStore.edit { it.remove(key) } - } catch (e: IOException) { - Timber.Forest.e(e) - } catch (e: Exception) { - Timber.Forest.e(e) - } - } - } + suspend fun removeFromDataStore(key: Preferences.Key) { + withContext(ioDispatcher) { + try { + context.dataStore.edit { it.remove(key) } + } catch (e: IOException) { + Timber.Forest.e(e) + } catch (e: Exception) { + Timber.Forest.e(e) + } + } + } - fun getFromStoreFlow(key: Preferences.Key) = context.dataStore.data.map { it[key] } + fun getFromStoreFlow(key: Preferences.Key) = context.dataStore.data.map { it[key] } - suspend fun getFromStore(key: Preferences.Key): T? { - return withContext(ioDispatcher) { - try { - context.dataStore.data.map { it[key] }.first() - } catch (e: IOException) { - Timber.Forest.e(e) - null - } - } - } + suspend fun getFromStore(key: Preferences.Key): T? { + return withContext(ioDispatcher) { + try { + context.dataStore.data.map { it[key] }.first() + } catch (e: IOException) { + Timber.Forest.e(e) + null + } + } + } - fun getFromStoreBlocking(key: Preferences.Key) = runBlocking { - context.dataStore.data.map { it[key] }.first() - } + fun getFromStoreBlocking(key: Preferences.Key) = runBlocking { + context.dataStore.data.map { it[key] }.first() + } - val preferencesFlow: Flow = context.dataStore.data.flowOn(ioDispatcher) + val preferencesFlow: Flow = context.dataStore.data.flowOn(ioDispatcher) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/DatabaseCallback.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/DatabaseCallback.kt index 9b8f4ef1..04cbfcfd 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/DatabaseCallback.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/DatabaseCallback.kt @@ -5,17 +5,18 @@ import androidx.sqlite.db.SupportSQLiteDatabase import timber.log.Timber class DatabaseCallback : RoomDatabase.Callback() { - override fun onCreate(db: SupportSQLiteDatabase) = db.run { - // Notice non-ui thread is here - beginTransaction() - try { - execSQL(Queries.createDefaultSettings()) - Timber.i("Bootstrapping settings data") - setTransactionSuccessful() - } catch (e: Exception) { - Timber.e(e) - } finally { - endTransaction() - } - } + override fun onCreate(db: SupportSQLiteDatabase) = + db.run { + // Notice non-ui thread is here + beginTransaction() + try { + execSQL(Queries.createDefaultSettings()) + Timber.i("Bootstrapping settings data") + setTransactionSuccessful() + } catch (e: Exception) { + Timber.e(e) + } finally { + endTransaction() + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/DatabaseListConverters.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/DatabaseListConverters.kt index 396d3221..e8316b1d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/DatabaseListConverters.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/DatabaseListConverters.kt @@ -4,20 +4,20 @@ import androidx.room.TypeConverter import kotlinx.serialization.json.Json class DatabaseListConverters { - @TypeConverter - fun listToString(value: MutableList): String { - return Json.encodeToString(value) - } + @TypeConverter + fun listToString(value: MutableList): String { + return Json.encodeToString(value) + } - @TypeConverter - fun stringToList(value: String): MutableList { - if (value.isBlank() || value.isEmpty()) return mutableListOf() - return try { - Json.decodeFromString>(value) - } catch (e: Exception) { - val list = value.split(",").toMutableList() - val json = listToString(list) - Json.decodeFromString>(json) - } - } + @TypeConverter + fun stringToList(value: String): MutableList { + if (value.isBlank() || value.isEmpty()) return mutableListOf() + return try { + Json.decodeFromString>(value) + } catch (e: Exception) { + val list = value.split(",").toMutableList() + val json = listToString(list) + Json.decodeFromString>(json) + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/Queries.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/Queries.kt index fdd5ddc2..039503c2 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/Queries.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/Queries.kt @@ -1,8 +1,8 @@ package com.zaneschepke.wireguardautotunnel.data object Queries { - fun createDefaultSettings(): String { - return """ + fun createDefaultSettings(): String { + return """ INSERT INTO Settings (is_tunnel_enabled, is_tunnel_on_mobile_data_enabled, trusted_network_ssids, @@ -24,12 +24,14 @@ object Queries { 'false', 'false', 'false') - """.trimIndent() - } + """ + .trimIndent() + } - fun createTunnelConfig(): String { - return """ + fun createTunnelConfig(): String { + return """ INSERT INTO TunnelConfig (name, wg_quick) VALUES ('test', 'test') - """.trimIndent() - } + """ + .trimIndent() + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/dao/SettingsDao.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/dao/SettingsDao.kt index ba9a3946..26092b85 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/dao/SettingsDao.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/dao/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/dao/TunnelConfigDao.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/dao/TunnelConfigDao.kt index 6bfbd454..f0e32af6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/dao/TunnelConfigDao.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/dao/TunnelConfigDao.kt @@ -11,48 +11,40 @@ 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: TunnelConfigs) + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: TunnelConfigs) - @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 WHERE name=:name") - suspend fun getByName(name: String): TunnelConfig? + @Query("SELECT * FROM TunnelConfig WHERE name=:name") + suspend fun getByName(name: String): TunnelConfig? - @Query("SELECT * FROM TunnelConfig WHERE is_Active=1") - suspend fun getActive(): TunnelConfigs + @Query("SELECT * FROM TunnelConfig WHERE is_Active=1") suspend fun getActive(): TunnelConfigs - @Query("SELECT * FROM TunnelConfig") - suspend fun getAll(): TunnelConfigs + @Query("SELECT * FROM TunnelConfig") suspend fun getAll(): TunnelConfigs - @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 WHERE tunnel_networks LIKE '%' || :name || '%'") - suspend fun findByTunnelNetworkName(name: String): TunnelConfigs + @Query("SELECT * FROM TunnelConfig WHERE tunnel_networks LIKE '%' || :name || '%'") + suspend fun findByTunnelNetworkName(name: String): TunnelConfigs - @Query("UPDATE TunnelConfig SET is_primary_tunnel = 0 WHERE is_primary_tunnel =1") - suspend fun resetPrimaryTunnel() + @Query("UPDATE TunnelConfig SET is_primary_tunnel = 0 WHERE is_primary_tunnel =1") + suspend fun resetPrimaryTunnel() - @Query("UPDATE TunnelConfig SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1") - suspend fun resetMobileDataTunnel() + @Query("UPDATE TunnelConfig SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1") + suspend fun resetMobileDataTunnel() - @Query("UPDATE TunnelConfig SET is_ethernet_tunnel = 0 WHERE is_ethernet_tunnel =1") - suspend fun resetEthernetTunnel() + @Query("UPDATE TunnelConfig SET is_ethernet_tunnel = 0 WHERE is_ethernet_tunnel =1") + suspend fun resetEthernetTunnel() - @Query("SELECT * FROM TUNNELCONFIG WHERE is_primary_tunnel=1") - suspend fun findByPrimary(): TunnelConfigs + @Query("SELECT * FROM TUNNELCONFIG WHERE is_primary_tunnel=1") + suspend fun findByPrimary(): TunnelConfigs - @Query("SELECT * FROM TUNNELCONFIG WHERE is_mobile_data_tunnel=1") - suspend fun findByMobileDataTunnel(): TunnelConfigs + @Query("SELECT * FROM TUNNELCONFIG WHERE is_mobile_data_tunnel=1") + suspend fun findByMobileDataTunnel(): TunnelConfigs - @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/model/GeneralState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/GeneralState.kt index b303436a..7e4dd897 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/GeneralState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/GeneralState.kt @@ -4,50 +4,52 @@ import com.zaneschepke.wireguardautotunnel.domain.entity.AppState import com.zaneschepke.wireguardautotunnel.ui.theme.Theme data class GeneralState( - val isLocationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT, - val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT, - val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT, - val isTunnelStatsExpanded: Boolean = IS_TUNNEL_STATS_EXPANDED, - val isLocalLogsEnabled: Boolean = IS_LOGS_ENABLED_DEFAULT, - val isRemoteControlEnabled: Boolean = IS_REMOTE_CONTROL_ENABLED, - val remoteKey: String? = null, - val locale: String? = null, - val theme: Theme = Theme.AUTOMATIC, + val isLocationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT, + val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT, + val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT, + val isTunnelStatsExpanded: Boolean = IS_TUNNEL_STATS_EXPANDED, + val isLocalLogsEnabled: Boolean = IS_LOGS_ENABLED_DEFAULT, + val isRemoteControlEnabled: Boolean = IS_REMOTE_CONTROL_ENABLED, + val remoteKey: String? = null, + val locale: String? = null, + val theme: Theme = Theme.AUTOMATIC, ) { - fun toAppState(): AppState = AppState( - isLocationDisclosureShown, - isBatteryOptimizationDisableShown, - isPinLockEnabled, - isTunnelStatsExpanded, - isLocationDisclosureShown, - isRemoteControlEnabled, - remoteKey, - locale, - theme, - ) + fun toAppState(): AppState = + AppState( + isLocationDisclosureShown, + isBatteryOptimizationDisableShown, + isPinLockEnabled, + isTunnelStatsExpanded, + isLocationDisclosureShown, + isRemoteControlEnabled, + remoteKey, + locale, + theme, + ) - companion object { - fun from(appState: AppState): GeneralState { - return with(appState) { - GeneralState( - isLocationDisclosureShown, - isBatteryOptimizationDisableShown, - isPinLockEnabled, - isTunnelStatsExpanded, - isLocalLogsEnabled, - isRemoteControlEnabled, - remoteKey, - locale, - theme, - ) - } - } - const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false - const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false - const val PIN_LOCK_ENABLED_DEFAULT = false - const val IS_TUNNEL_STATS_EXPANDED = false - const val IS_LOGS_ENABLED_DEFAULT = false - const val IS_REMOTE_CONTROL_ENABLED = false - } + companion object { + fun from(appState: AppState): GeneralState { + return with(appState) { + GeneralState( + isLocationDisclosureShown, + isBatteryOptimizationDisableShown, + isPinLockEnabled, + isTunnelStatsExpanded, + isLocalLogsEnabled, + isRemoteControlEnabled, + remoteKey, + locale, + theme, + ) + } + } + + const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false + const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false + const val PIN_LOCK_ENABLED_DEFAULT = false + const val IS_TUNNEL_STATS_EXPANDED = false + const val IS_LOGS_ENABLED_DEFAULT = false + const val IS_REMOTE_CONTROL_ENABLED = false + } } 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 9af43514..c3903706 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 @@ -7,112 +7,100 @@ import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings @Entity data class Settings( - @PrimaryKey(autoGenerate = true) val id: Int = 0, - @ColumnInfo(name = "is_tunnel_enabled") val isAutoTunnelEnabled: Boolean = false, - @ColumnInfo(name = "is_tunnel_on_mobile_data_enabled") - val isTunnelOnMobileDataEnabled: Boolean = false, - @ColumnInfo(name = "trusted_network_ssids") - val trustedNetworkSSIDs: MutableList = mutableListOf(), - @ColumnInfo(name = "is_always_on_vpn_enabled") val isAlwaysOnVpnEnabled: Boolean = false, - @ColumnInfo(name = "is_tunnel_on_ethernet_enabled") - val isTunnelOnEthernetEnabled: Boolean = false, - @ColumnInfo( - name = "is_shortcuts_enabled", - defaultValue = "false", - ) - val isShortcutsEnabled: Boolean = false, - @ColumnInfo( - name = "is_tunnel_on_wifi_enabled", - defaultValue = "false", - ) - val isTunnelOnWifiEnabled: Boolean = false, - @ColumnInfo( - name = "is_kernel_enabled", - defaultValue = "false", - ) - val isKernelEnabled: Boolean = false, - @ColumnInfo( - name = "is_restore_on_boot_enabled", - defaultValue = "false", - ) - val isRestoreOnBootEnabled: Boolean = false, - @ColumnInfo( - name = "is_multi_tunnel_enabled", - defaultValue = "false", - ) - val isMultiTunnelEnabled: Boolean = false, - @ColumnInfo( - name = "is_ping_enabled", - defaultValue = "false", - ) - val isPingEnabled: Boolean = false, - @ColumnInfo( - name = "is_amnezia_enabled", - defaultValue = "false", - ) - val isAmneziaEnabled: Boolean = false, - @ColumnInfo( - name = "is_wildcards_enabled", - defaultValue = "false", - ) - val isWildcardsEnabled: Boolean = false, - @ColumnInfo( - name = "is_wifi_by_shell_enabled", - defaultValue = "false", - ) - val isWifiNameByShellEnabled: Boolean = false, - @ColumnInfo( - name = "is_stop_on_no_internet_enabled", - defaultValue = "false", - ) - val isStopOnNoInternetEnabled: Boolean = false, - @ColumnInfo( - name = "is_vpn_kill_switch_enabled", - defaultValue = "false", - ) - val isVpnKillSwitchEnabled: Boolean = false, - @ColumnInfo( - name = "is_kernel_kill_switch_enabled", - defaultValue = "false", - ) - val isKernelKillSwitchEnabled: Boolean = false, - @ColumnInfo( - name = "is_lan_on_kill_switch_enabled", - defaultValue = "false", - ) - val isLanOnKillSwitchEnabled: Boolean = false, - @ColumnInfo( - name = "debounce_delay_seconds", - defaultValue = "3", - ) - val debounceDelaySeconds: Int = 3, - @ColumnInfo( - name = "is_disable_kill_switch_on_trusted_enabled", - defaultValue = "false", - ) - val isDisableKillSwitchOnTrustedEnabled: Boolean = false, + @PrimaryKey(autoGenerate = true) val id: Int = 0, + @ColumnInfo(name = "is_tunnel_enabled") val isAutoTunnelEnabled: Boolean = false, + @ColumnInfo(name = "is_tunnel_on_mobile_data_enabled") + val isTunnelOnMobileDataEnabled: Boolean = false, + @ColumnInfo(name = "trusted_network_ssids") + val trustedNetworkSSIDs: MutableList = mutableListOf(), + @ColumnInfo(name = "is_always_on_vpn_enabled") val isAlwaysOnVpnEnabled: Boolean = false, + @ColumnInfo(name = "is_tunnel_on_ethernet_enabled") + val isTunnelOnEthernetEnabled: Boolean = false, + @ColumnInfo(name = "is_shortcuts_enabled", defaultValue = "false") + val isShortcutsEnabled: Boolean = false, + @ColumnInfo(name = "is_tunnel_on_wifi_enabled", defaultValue = "false") + val isTunnelOnWifiEnabled: Boolean = false, + @ColumnInfo(name = "is_kernel_enabled", defaultValue = "false") + val isKernelEnabled: Boolean = false, + @ColumnInfo(name = "is_restore_on_boot_enabled", defaultValue = "false") + val isRestoreOnBootEnabled: Boolean = false, + @ColumnInfo(name = "is_multi_tunnel_enabled", defaultValue = "false") + val isMultiTunnelEnabled: Boolean = false, + @ColumnInfo(name = "is_ping_enabled", defaultValue = "false") + val isPingEnabled: Boolean = false, + @ColumnInfo(name = "is_amnezia_enabled", defaultValue = "false") + val isAmneziaEnabled: Boolean = false, + @ColumnInfo(name = "is_wildcards_enabled", defaultValue = "false") + val isWildcardsEnabled: Boolean = false, + @ColumnInfo(name = "is_wifi_by_shell_enabled", defaultValue = "false") + val isWifiNameByShellEnabled: Boolean = false, + @ColumnInfo(name = "is_stop_on_no_internet_enabled", defaultValue = "false") + val isStopOnNoInternetEnabled: Boolean = false, + @ColumnInfo(name = "is_vpn_kill_switch_enabled", defaultValue = "false") + val isVpnKillSwitchEnabled: Boolean = false, + @ColumnInfo(name = "is_kernel_kill_switch_enabled", defaultValue = "false") + val isKernelKillSwitchEnabled: Boolean = false, + @ColumnInfo(name = "is_lan_on_kill_switch_enabled", defaultValue = "false") + val isLanOnKillSwitchEnabled: Boolean = false, + @ColumnInfo(name = "debounce_delay_seconds", defaultValue = "3") + val debounceDelaySeconds: Int = 3, + @ColumnInfo(name = "is_disable_kill_switch_on_trusted_enabled", defaultValue = "false") + val isDisableKillSwitchOnTrustedEnabled: Boolean = false, ) { - fun toAppSettings(): AppSettings { - return AppSettings( - id, isAutoTunnelEnabled, isTunnelOnMobileDataEnabled, trustedNetworkSSIDs, isAlwaysOnVpnEnabled, isTunnelOnEthernetEnabled, - isShortcutsEnabled, isTunnelOnWifiEnabled, isKernelEnabled, isRestoreOnBootEnabled, isMultiTunnelEnabled, isPingEnabled, - isAmneziaEnabled, isWildcardsEnabled, isWifiNameByShellEnabled, isStopOnNoInternetEnabled, isVpnKillSwitchEnabled, - isKernelKillSwitchEnabled, isLanOnKillSwitchEnabled, debounceDelaySeconds, isDisableKillSwitchOnTrustedEnabled, - ) - } + fun toAppSettings(): AppSettings { + return AppSettings( + id, + isAutoTunnelEnabled, + isTunnelOnMobileDataEnabled, + trustedNetworkSSIDs, + isAlwaysOnVpnEnabled, + isTunnelOnEthernetEnabled, + isShortcutsEnabled, + isTunnelOnWifiEnabled, + isKernelEnabled, + isRestoreOnBootEnabled, + isMultiTunnelEnabled, + isPingEnabled, + isAmneziaEnabled, + isWildcardsEnabled, + isWifiNameByShellEnabled, + isStopOnNoInternetEnabled, + isVpnKillSwitchEnabled, + isKernelKillSwitchEnabled, + isLanOnKillSwitchEnabled, + debounceDelaySeconds, + isDisableKillSwitchOnTrustedEnabled, + ) + } - companion object { - fun from(appSettings: AppSettings): Settings { - return with(appSettings) { - Settings( - id, isAutoTunnelEnabled, isTunnelOnMobileDataEnabled, trustedNetworkSSIDs.toMutableList(), isAlwaysOnVpnEnabled, - isTunnelOnEthernetEnabled, isShortcutsEnabled, isTunnelOnWifiEnabled, isKernelEnabled, isRestoreOnBootEnabled, - isMultiTunnelEnabled, isPingEnabled, isAmneziaEnabled, isWildcardsEnabled, isWifiNameByShellEnabled, - isStopOnNoInternetEnabled, isVpnKillSwitchEnabled, isKernelKillSwitchEnabled, isLanOnKillSwitchEnabled, - debounceDelaySeconds, isDisableKillSwitchOnTrustedEnabled, - ) - } - } - } + companion object { + fun from(appSettings: AppSettings): Settings { + return with(appSettings) { + Settings( + id, + isAutoTunnelEnabled, + isTunnelOnMobileDataEnabled, + trustedNetworkSSIDs.toMutableList(), + isAlwaysOnVpnEnabled, + isTunnelOnEthernetEnabled, + isShortcutsEnabled, + isTunnelOnWifiEnabled, + isKernelEnabled, + isRestoreOnBootEnabled, + isMultiTunnelEnabled, + isPingEnabled, + isAmneziaEnabled, + isWildcardsEnabled, + isWifiNameByShellEnabled, + isStopOnNoInternetEnabled, + isVpnKillSwitchEnabled, + isKernelKillSwitchEnabled, + isLanOnKillSwitchEnabled, + debounceDelaySeconds, + isDisableKillSwitchOnTrustedEnabled, + ) + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/TunnelConfig.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/TunnelConfig.kt index f89ffcdc..d67ddf8a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/TunnelConfig.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/TunnelConfig.kt @@ -8,86 +8,70 @@ import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf @Entity(indices = [Index(value = ["name"], unique = true)]) data class TunnelConfig( - @PrimaryKey(autoGenerate = true) val id: Int = 0, - @ColumnInfo(name = "name") val name: String, - @ColumnInfo(name = "wg_quick") val wgQuick: String, - @ColumnInfo( - name = "tunnel_networks", - defaultValue = "", - ) - val tunnelNetworks: MutableList = mutableListOf(), - @ColumnInfo( - name = "is_mobile_data_tunnel", - defaultValue = "false", - ) - val isMobileDataTunnel: Boolean = false, - @ColumnInfo( - name = "is_primary_tunnel", - defaultValue = "false", - ) - val isPrimaryTunnel: Boolean = false, - @ColumnInfo( - name = "am_quick", - defaultValue = "", - ) - val amQuick: String = AM_QUICK_DEFAULT, - @ColumnInfo( - name = "is_Active", - defaultValue = "false", - ) - val isActive: Boolean = false, - @ColumnInfo( - name = "is_ping_enabled", - defaultValue = "false", - ) - val isPingEnabled: Boolean = false, - @ColumnInfo( - name = "ping_interval", - defaultValue = "null", - ) - val pingInterval: Long? = null, - @ColumnInfo( - name = "ping_cooldown", - defaultValue = "null", - ) - val pingCooldown: Long? = null, - @ColumnInfo( - name = "ping_ip", - defaultValue = "null", - ) - var pingIp: String? = null, - @ColumnInfo( - name = "is_ethernet_tunnel", - defaultValue = "false", - ) - var isEthernetTunnel: Boolean = false, - @ColumnInfo( - name = "is_ipv4_preferred", - defaultValue = "true", - ) - var isIpv4Preferred: Boolean = true, + @PrimaryKey(autoGenerate = true) val id: Int = 0, + @ColumnInfo(name = "name") val name: String, + @ColumnInfo(name = "wg_quick") val wgQuick: String, + @ColumnInfo(name = "tunnel_networks", defaultValue = "") + val tunnelNetworks: MutableList = mutableListOf(), + @ColumnInfo(name = "is_mobile_data_tunnel", defaultValue = "false") + val isMobileDataTunnel: Boolean = false, + @ColumnInfo(name = "is_primary_tunnel", defaultValue = "false") + val isPrimaryTunnel: Boolean = false, + @ColumnInfo(name = "am_quick", defaultValue = "") val amQuick: String = AM_QUICK_DEFAULT, + @ColumnInfo(name = "is_Active", defaultValue = "false") val isActive: Boolean = false, + @ColumnInfo(name = "is_ping_enabled", defaultValue = "false") + val isPingEnabled: Boolean = false, + @ColumnInfo(name = "ping_interval", defaultValue = "null") val pingInterval: Long? = null, + @ColumnInfo(name = "ping_cooldown", defaultValue = "null") val pingCooldown: Long? = null, + @ColumnInfo(name = "ping_ip", defaultValue = "null") var pingIp: String? = null, + @ColumnInfo(name = "is_ethernet_tunnel", defaultValue = "false") + var isEthernetTunnel: Boolean = false, + @ColumnInfo(name = "is_ipv4_preferred", defaultValue = "true") + var isIpv4Preferred: Boolean = true, ) { - fun toTunnel(): TunnelConf { - return TunnelConf( - id, name, wgQuick, tunnelNetworks, isMobileDataTunnel, - isPrimaryTunnel, amQuick, isActive, isPingEnabled, pingInterval, - pingCooldown, pingIp, isEthernetTunnel, isIpv4Preferred, - ) - } + fun toTunnel(): TunnelConf { + return TunnelConf( + id, + name, + wgQuick, + tunnelNetworks, + isMobileDataTunnel, + isPrimaryTunnel, + amQuick, + isActive, + isPingEnabled, + pingInterval, + pingCooldown, + pingIp, + isEthernetTunnel, + isIpv4Preferred, + ) + } - companion object { + companion object { - const val AM_QUICK_DEFAULT = "" + const val AM_QUICK_DEFAULT = "" - fun from(tunnelConf: TunnelConf): TunnelConfig { - return with(tunnelConf) { - return TunnelConfig( - id, tunName, wgQuick, tunnelNetworks.toMutableList(), isMobileDataTunnel, - isPrimaryTunnel, amQuick, isActive, isPingEnabled, pingInterval, - pingCooldown, pingIp, isEthernetTunnel, isIpv4Preferred, - ) - } - } - } + fun from(tunnelConf: TunnelConf): TunnelConfig { + return with(tunnelConf) { + return TunnelConfig( + id, + tunName, + wgQuick, + tunnelNetworks.toMutableList(), + isMobileDataTunnel, + isPrimaryTunnel, + amQuick, + isActive, + isPingEnabled, + pingInterval, + pingCooldown, + pingIp, + isEthernetTunnel, + isIpv4Preferred, + ) + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRoomRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRoomRepository.kt index 01aace07..d2452d6c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRoomRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRoomRepository.kt @@ -10,19 +10,19 @@ import javax.inject.Inject class AppDataRoomRepository @Inject constructor( - override val settings: AppSettingRepository, - override val tunnels: TunnelRepository, - override val appState: AppStateRepository, + override val settings: AppSettingRepository, + override val tunnels: TunnelRepository, + override val appState: AppStateRepository, ) : AppDataRepository { - override suspend fun getPrimaryOrFirstTunnel(): TunnelConf? { - return tunnels.findPrimary().firstOrNull() ?: tunnels.getAll().firstOrNull() - } + override suspend fun getPrimaryOrFirstTunnel(): TunnelConf? { + return tunnels.findPrimary().firstOrNull() ?: tunnels.getAll().firstOrNull() + } - override suspend fun getStartTunnelConfig(): TunnelConf? { - tunnels.getActive().let { - if (it.isNotEmpty()) return it.first() - return getPrimaryOrFirstTunnel() - } - } + override suspend fun getStartTunnelConfig(): TunnelConf? { + tunnels.getActive().let { + if (it.isNotEmpty()) return it.first() + return getPrimaryOrFirstTunnel() + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt index 25e86c05..5e926703 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt @@ -9,117 +9,125 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import timber.log.Timber -class DataStoreAppStateRepository( - private val dataStoreManager: DataStoreManager, -) : - AppStateRepository { - override suspend fun isLocationDisclosureShown(): Boolean { - return dataStoreManager.getFromStore(DataStoreManager.locationDisclosureShown) - ?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT - } +class DataStoreAppStateRepository(private val dataStoreManager: DataStoreManager) : + AppStateRepository { + override suspend fun isLocationDisclosureShown(): Boolean { + return dataStoreManager.getFromStore(DataStoreManager.locationDisclosureShown) + ?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT + } - override suspend fun setLocationDisclosureShown(shown: Boolean) { - dataStoreManager.saveToDataStore(DataStoreManager.locationDisclosureShown, shown) - } + override suspend fun setLocationDisclosureShown(shown: Boolean) { + dataStoreManager.saveToDataStore(DataStoreManager.locationDisclosureShown, shown) + } - override suspend fun isPinLockEnabled(): Boolean { - return dataStoreManager.getFromStore(DataStoreManager.pinLockEnabled) - ?: GeneralState.PIN_LOCK_ENABLED_DEFAULT - } + override suspend fun isPinLockEnabled(): Boolean { + return dataStoreManager.getFromStore(DataStoreManager.pinLockEnabled) + ?: GeneralState.PIN_LOCK_ENABLED_DEFAULT + } - override suspend fun setPinLockEnabled(enabled: Boolean) { - dataStoreManager.saveToDataStore(DataStoreManager.pinLockEnabled, enabled) - } + override suspend fun setPinLockEnabled(enabled: Boolean) { + dataStoreManager.saveToDataStore(DataStoreManager.pinLockEnabled, enabled) + } - override suspend fun isBatteryOptimizationDisableShown(): Boolean { - return dataStoreManager.getFromStore(DataStoreManager.batteryDisableShown) - ?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT - } + override suspend fun isBatteryOptimizationDisableShown(): Boolean { + return dataStoreManager.getFromStore(DataStoreManager.batteryDisableShown) + ?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT + } - override suspend fun setBatteryOptimizationDisableShown(shown: Boolean) { - dataStoreManager.saveToDataStore(DataStoreManager.batteryDisableShown, shown) - } + override suspend fun setBatteryOptimizationDisableShown(shown: Boolean) { + dataStoreManager.saveToDataStore(DataStoreManager.batteryDisableShown, shown) + } - override suspend fun isTunnelStatsExpanded(): Boolean { - return dataStoreManager.getFromStore(DataStoreManager.tunnelStatsExpanded) - ?: GeneralState.IS_TUNNEL_STATS_EXPANDED - } + override suspend fun isTunnelStatsExpanded(): Boolean { + return dataStoreManager.getFromStore(DataStoreManager.tunnelStatsExpanded) + ?: GeneralState.IS_TUNNEL_STATS_EXPANDED + } - override suspend fun setTunnelStatsExpanded(expanded: Boolean) { - dataStoreManager.saveToDataStore(DataStoreManager.tunnelStatsExpanded, expanded) - } + override suspend fun setTunnelStatsExpanded(expanded: Boolean) { + dataStoreManager.saveToDataStore(DataStoreManager.tunnelStatsExpanded, expanded) + } - override suspend fun setTheme(theme: Theme) { - dataStoreManager.saveToDataStore(DataStoreManager.theme, theme.name) - } + override suspend fun setTheme(theme: Theme) { + dataStoreManager.saveToDataStore(DataStoreManager.theme, theme.name) + } - override suspend fun getTheme(): Theme { - return dataStoreManager.getFromStore(DataStoreManager.theme)?.let { - try { - Theme.valueOf(it) - } catch (_: IllegalArgumentException) { - Theme.AUTOMATIC - } - } ?: Theme.AUTOMATIC - } + override suspend fun getTheme(): Theme { + return dataStoreManager.getFromStore(DataStoreManager.theme)?.let { + try { + Theme.valueOf(it) + } catch (_: IllegalArgumentException) { + Theme.AUTOMATIC + } + } ?: Theme.AUTOMATIC + } - override suspend fun isLocalLogsEnabled(): Boolean { - return dataStoreManager.getFromStore(DataStoreManager.isLocalLogsEnabled) ?: GeneralState.IS_LOGS_ENABLED_DEFAULT - } + override suspend fun isLocalLogsEnabled(): Boolean { + return dataStoreManager.getFromStore(DataStoreManager.isLocalLogsEnabled) + ?: GeneralState.IS_LOGS_ENABLED_DEFAULT + } - override suspend fun setLocalLogsEnabled(enabled: Boolean) { - dataStoreManager.saveToDataStore(DataStoreManager.isLocalLogsEnabled, enabled) - } + override suspend fun setLocalLogsEnabled(enabled: Boolean) { + dataStoreManager.saveToDataStore(DataStoreManager.isLocalLogsEnabled, enabled) + } - override suspend fun setLocale(localeTag: String) { - dataStoreManager.saveToDataStore(DataStoreManager.locale, localeTag) - } + override suspend fun setLocale(localeTag: String) { + dataStoreManager.saveToDataStore(DataStoreManager.locale, localeTag) + } - override suspend fun getLocale(): String? { - return dataStoreManager.getFromStore(DataStoreManager.locale) - } + override suspend fun getLocale(): String? { + return dataStoreManager.getFromStore(DataStoreManager.locale) + } - override suspend fun setIsRemoteControlEnabled(enabled: Boolean) { - dataStoreManager.saveToDataStore(DataStoreManager.isRemoteControlEnabled, enabled) - } + override suspend fun setIsRemoteControlEnabled(enabled: Boolean) { + dataStoreManager.saveToDataStore(DataStoreManager.isRemoteControlEnabled, enabled) + } - override suspend fun isRemoteControlEnabled(): Boolean { - return dataStoreManager.getFromStore(DataStoreManager.isRemoteControlEnabled) ?: GeneralState.IS_REMOTE_CONTROL_ENABLED - } + override suspend fun isRemoteControlEnabled(): Boolean { + return dataStoreManager.getFromStore(DataStoreManager.isRemoteControlEnabled) + ?: GeneralState.IS_REMOTE_CONTROL_ENABLED + } - override suspend fun setRemoteKey(key: String) { - dataStoreManager.saveToDataStore(DataStoreManager.remoteKey, key) - } + override suspend fun setRemoteKey(key: String) { + dataStoreManager.saveToDataStore(DataStoreManager.remoteKey, key) + } - override suspend fun getRemoteKey(): String? { - return dataStoreManager.getFromStore(DataStoreManager.remoteKey) - } + override suspend fun getRemoteKey(): String? { + return dataStoreManager.getFromStore(DataStoreManager.remoteKey) + } - override val flow: Flow = - dataStoreManager.preferencesFlow.map { prefs -> - prefs?.let { pref -> - try { - GeneralState( - isLocationDisclosureShown = - pref[DataStoreManager.locationDisclosureShown] - ?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT, - isBatteryOptimizationDisableShown = - pref[DataStoreManager.batteryDisableShown] - ?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT, - isPinLockEnabled = - pref[DataStoreManager.pinLockEnabled] - ?: GeneralState.PIN_LOCK_ENABLED_DEFAULT, - isTunnelStatsExpanded = pref[DataStoreManager.tunnelStatsExpanded] ?: GeneralState.IS_TUNNEL_STATS_EXPANDED, - isLocalLogsEnabled = pref[DataStoreManager.isLocalLogsEnabled] ?: GeneralState.IS_LOGS_ENABLED_DEFAULT, - isRemoteControlEnabled = pref[DataStoreManager.isRemoteControlEnabled] ?: GeneralState.IS_REMOTE_CONTROL_ENABLED, - remoteKey = pref[DataStoreManager.remoteKey], - locale = pref[DataStoreManager.locale], - theme = getTheme(), - ) - } catch (e: IllegalArgumentException) { - Timber.e(e) - GeneralState() - } - } ?: GeneralState() - }.map { it.toAppState() } + override val flow: Flow = + dataStoreManager.preferencesFlow + .map { prefs -> + prefs?.let { pref -> + try { + GeneralState( + isLocationDisclosureShown = + pref[DataStoreManager.locationDisclosureShown] + ?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT, + isBatteryOptimizationDisableShown = + pref[DataStoreManager.batteryDisableShown] + ?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT, + isPinLockEnabled = + pref[DataStoreManager.pinLockEnabled] + ?: GeneralState.PIN_LOCK_ENABLED_DEFAULT, + isTunnelStatsExpanded = + pref[DataStoreManager.tunnelStatsExpanded] + ?: GeneralState.IS_TUNNEL_STATS_EXPANDED, + isLocalLogsEnabled = + pref[DataStoreManager.isLocalLogsEnabled] + ?: GeneralState.IS_LOGS_ENABLED_DEFAULT, + isRemoteControlEnabled = + pref[DataStoreManager.isRemoteControlEnabled] + ?: GeneralState.IS_REMOTE_CONTROL_ENABLED, + remoteKey = pref[DataStoreManager.remoteKey], + locale = pref[DataStoreManager.locale], + theme = getTheme(), + ) + } catch (e: IllegalArgumentException) { + Timber.e(e) + GeneralState() + } + } ?: GeneralState() + } + .map { it.toAppState() } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomSettingsRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomSettingsRepository.kt index 038b0dfd..6fa04f2b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomSettingsRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomSettingsRepository.kt @@ -11,21 +11,20 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext class RoomSettingsRepository( - private val settingsDoa: SettingsDao, - @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + private val settingsDoa: SettingsDao, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, ) : AppSettingRepository { - override suspend fun save(appSettings: AppSettings) { - withContext(ioDispatcher) { - settingsDoa.save(Settings.from(appSettings)) - } - } + override suspend fun save(appSettings: AppSettings) { + withContext(ioDispatcher) { settingsDoa.save(Settings.from(appSettings)) } + } - override val flow = settingsDoa.getSettingsFlow().flowOn(ioDispatcher).map { it.toAppSettings() } + override val flow = + settingsDoa.getSettingsFlow().flowOn(ioDispatcher).map { it.toAppSettings() } - override suspend fun get(): AppSettings { - return withContext(ioDispatcher) { - (settingsDoa.getAll().firstOrNull() ?: Settings()).toAppSettings() - } - } + override suspend fun get(): AppSettings { + return withContext(ioDispatcher) { + (settingsDoa.getAll().firstOrNull() ?: Settings()).toAppSettings() + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomTunnelRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomTunnelRepository.kt index 68babfd2..a9d91fd5 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomTunnelRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomTunnelRepository.kt @@ -12,102 +12,81 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext class RoomTunnelRepository( - private val tunnelConfigDao: TunnelConfigDao, - @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + private val tunnelConfigDao: TunnelConfigDao, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, ) : TunnelRepository { - override val flow = tunnelConfigDao.getAllFlow().flowOn(ioDispatcher).map { it.map { it.toTunnel() } } + override val flow = + tunnelConfigDao.getAllFlow().flowOn(ioDispatcher).map { it.map { it.toTunnel() } } - override suspend fun getAll(): Tunnels { - return withContext(ioDispatcher) { - tunnelConfigDao.getAll().map { it.toTunnel() } - } - } + override suspend fun getAll(): Tunnels { + return withContext(ioDispatcher) { tunnelConfigDao.getAll().map { it.toTunnel() } } + } - override suspend fun save(tunnelConf: TunnelConf) { - withContext(ioDispatcher) { - tunnelConfigDao.save(TunnelConfig.from(tunnelConf)) - } - } + override suspend fun save(tunnelConf: TunnelConf) { + withContext(ioDispatcher) { tunnelConfigDao.save(TunnelConfig.from(tunnelConf)) } + } - override suspend fun saveAll(tunnelConfList: List) { - withContext(ioDispatcher) { - tunnelConfigDao.saveAll(tunnelConfList.map(TunnelConfig::from)) - } - } + override suspend fun saveAll(tunnelConfList: List) { + withContext(ioDispatcher) { + tunnelConfigDao.saveAll(tunnelConfList.map(TunnelConfig::from)) + } + } - override suspend fun updatePrimaryTunnel(tunnelConf: TunnelConf?) { - withContext(ioDispatcher) { - tunnelConfigDao.resetPrimaryTunnel() - tunnelConf?.let { - save( - it.copy( - isPrimaryTunnel = true, - ), - ) - } - } - } + override suspend fun updatePrimaryTunnel(tunnelConf: TunnelConf?) { + withContext(ioDispatcher) { + tunnelConfigDao.resetPrimaryTunnel() + tunnelConf?.let { save(it.copy(isPrimaryTunnel = true)) } + } + } - override suspend fun updateMobileDataTunnel(tunnelConf: TunnelConf?) { - withContext(ioDispatcher) { - tunnelConfigDao.resetMobileDataTunnel() - tunnelConf?.let { - save( - it.copy( - isMobileDataTunnel = true, - ), - ) - } - } - } + override suspend fun updateMobileDataTunnel(tunnelConf: TunnelConf?) { + withContext(ioDispatcher) { + tunnelConfigDao.resetMobileDataTunnel() + tunnelConf?.let { save(it.copy(isMobileDataTunnel = true)) } + } + } - override suspend fun updateEthernetTunnel(tunnelConf: TunnelConf?) { - withContext(ioDispatcher) { - tunnelConfigDao.resetEthernetTunnel() - tunnelConf?.let { - save( - it.copy( - isEthernetTunnel = true, - ), - ) - } - } - } + override suspend fun updateEthernetTunnel(tunnelConf: TunnelConf?) { + withContext(ioDispatcher) { + tunnelConfigDao.resetEthernetTunnel() + tunnelConf?.let { save(it.copy(isEthernetTunnel = true)) } + } + } - override suspend fun delete(tunnelConf: TunnelConf) { - withContext(ioDispatcher) { - tunnelConfigDao.delete(TunnelConfig.from(tunnelConf)) - } - } + override suspend fun delete(tunnelConf: TunnelConf) { + withContext(ioDispatcher) { tunnelConfigDao.delete(TunnelConfig.from(tunnelConf)) } + } - override suspend fun getById(id: Int): TunnelConf? { - return withContext(ioDispatcher) { tunnelConfigDao.getById(id.toLong())?.toTunnel() } - } + override suspend fun getById(id: Int): TunnelConf? { + return withContext(ioDispatcher) { tunnelConfigDao.getById(id.toLong())?.toTunnel() } + } - override suspend fun getActive(): Tunnels { - return withContext(ioDispatcher) { - tunnelConfigDao.getActive().map { it.toTunnel() } - } - } + override suspend fun getActive(): Tunnels { + return withContext(ioDispatcher) { tunnelConfigDao.getActive().map { it.toTunnel() } } + } - override suspend fun count(): Int { - return withContext(ioDispatcher) { tunnelConfigDao.count().toInt() } - } + override suspend fun count(): Int { + return withContext(ioDispatcher) { tunnelConfigDao.count().toInt() } + } - override suspend fun findByTunnelName(name: String): TunnelConf? { - return withContext(ioDispatcher) { tunnelConfigDao.getByName(name)?.toTunnel() } - } + override suspend fun findByTunnelName(name: String): TunnelConf? { + return withContext(ioDispatcher) { tunnelConfigDao.getByName(name)?.toTunnel() } + } - override suspend fun findByTunnelNetworksName(name: String): Tunnels { - return withContext(ioDispatcher) { tunnelConfigDao.findByTunnelNetworkName(name).map { it.toTunnel() } } - } + override suspend fun findByTunnelNetworksName(name: String): Tunnels { + return withContext(ioDispatcher) { + tunnelConfigDao.findByTunnelNetworkName(name).map { it.toTunnel() } + } + } - override suspend fun findByMobileDataTunnel(): Tunnels { - return withContext(ioDispatcher) { tunnelConfigDao.findByMobileDataTunnel().map { it.toTunnel() } } - } + override suspend fun findByMobileDataTunnel(): Tunnels { + return withContext(ioDispatcher) { + tunnelConfigDao.findByMobileDataTunnel().map { it.toTunnel() } + } + } - override suspend fun findPrimary(): Tunnels { - return withContext(ioDispatcher) { tunnelConfigDao.findByPrimary().map { it.toTunnel() } } - } + override suspend fun findPrimary(): Tunnels { + return withContext(ioDispatcher) { tunnelConfigDao.findByPrimary().map { it.toTunnel() } } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/AppModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/AppModule.kt index b0e60013..392c8dab 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/AppModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/AppModule.kt @@ -12,35 +12,39 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob -import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) class AppModule { - @Singleton - @ApplicationScope - @Provides - fun providesApplicationScope(@DefaultDispatcher defaultDispatcher: CoroutineDispatcher): CoroutineScope = - CoroutineScope(SupervisorJob() + defaultDispatcher) + @Singleton + @ApplicationScope + @Provides + fun providesApplicationScope( + @DefaultDispatcher defaultDispatcher: CoroutineDispatcher + ): CoroutineScope = CoroutineScope(SupervisorJob() + defaultDispatcher) - @Singleton - @Provides - fun provideLogCollect(@ApplicationContext context: Context): LogReader { - return LogcatReader.init(storageDir = context.filesDir.absolutePath) - } + @Singleton + @Provides + fun provideLogCollect(@ApplicationContext context: Context): LogReader { + return LogcatReader.init(storageDir = context.filesDir.absolutePath) + } - @Singleton - @Provides - fun provideNotificationService(@ApplicationContext context: Context): NotificationManager { - return WireGuardNotification(context) - } + @Singleton + @Provides + fun provideNotificationService(@ApplicationContext context: Context): NotificationManager { + return WireGuardNotification(context) + } - @Singleton - @Provides - fun provideShortcutManager(@ApplicationContext context: Context, @IoDispatcher ioDispatcher: CoroutineDispatcher): ShortcutManager { - return DynamicShortcutManager(context, ioDispatcher) - } + @Singleton + @Provides + fun provideShortcutManager( + @ApplicationContext context: Context, + @IoDispatcher ioDispatcher: CoroutineDispatcher, + ): ShortcutManager { + return DynamicShortcutManager(context, ioDispatcher) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/BackendQualifiers.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/BackendQualifiers.kt index 9d24a562..8152764a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/BackendQualifiers.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/BackendQualifiers.kt @@ -2,18 +2,10 @@ package com.zaneschepke.wireguardautotunnel.di import javax.inject.Qualifier -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class TunnelShell +@Qualifier @Retention(AnnotationRetention.BINARY) annotation class TunnelShell -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class AppShell +@Qualifier @Retention(AnnotationRetention.BINARY) annotation class AppShell -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class Kernel +@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Kernel -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class Userspace +@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Userspace diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/CoroutineQualifiers.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/CoroutineQualifiers.kt index 144fffe7..4f95c80e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/CoroutineQualifiers.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/CoroutineQualifiers.kt @@ -2,26 +2,14 @@ package com.zaneschepke.wireguardautotunnel.di import javax.inject.Qualifier -@Retention(AnnotationRetention.RUNTIME) -@Qualifier -annotation class DefaultDispatcher +@Retention(AnnotationRetention.RUNTIME) @Qualifier annotation class DefaultDispatcher -@Retention(AnnotationRetention.RUNTIME) -@Qualifier -annotation class IoDispatcher +@Retention(AnnotationRetention.RUNTIME) @Qualifier annotation class IoDispatcher -@Retention(AnnotationRetention.RUNTIME) -@Qualifier -annotation class MainDispatcher +@Retention(AnnotationRetention.RUNTIME) @Qualifier annotation class MainDispatcher -@Retention(AnnotationRetention.BINARY) -@Qualifier -annotation class MainImmediateDispatcher +@Retention(AnnotationRetention.BINARY) @Qualifier annotation class MainImmediateDispatcher -@Retention(AnnotationRetention.RUNTIME) -@Qualifier -annotation class ApplicationScope +@Retention(AnnotationRetention.RUNTIME) @Qualifier annotation class ApplicationScope -@Retention(AnnotationRetention.RUNTIME) -@Qualifier -annotation class ServiceScope +@Retention(AnnotationRetention.RUNTIME) @Qualifier annotation class ServiceScope diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/CoroutinesDispatchersModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/CoroutinesDispatchersModule.kt index 94ab7b5f..0bacea8d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/CoroutinesDispatchersModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/CoroutinesDispatchersModule.kt @@ -10,19 +10,15 @@ import kotlinx.coroutines.Dispatchers @Module @InstallIn(SingletonComponent::class) object CoroutinesDispatchersModule { - @DefaultDispatcher - @Provides - fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default + @DefaultDispatcher + @Provides + fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default - @IoDispatcher - @Provides - fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO + @IoDispatcher @Provides fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO - @MainDispatcher - @Provides - fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main + @MainDispatcher @Provides fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main - @MainImmediateDispatcher - @Provides - fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate + @MainImmediateDispatcher + @Provides + fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/RepositoryModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/RepositoryModule.kt index 032bff6b..faf7eabe 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/RepositoryModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/RepositoryModule.kt @@ -21,68 +21,77 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.CoroutineDispatcher import javax.inject.Singleton +import kotlinx.coroutines.CoroutineDispatcher @Module @InstallIn(SingletonComponent::class) class RepositoryModule { - @Provides - @Singleton - fun provideDatabase(@ApplicationContext context: Context): AppDatabase { - return Room.databaseBuilder( - context, - AppDatabase::class.java, - context.getString(R.string.db_name), - ) - .fallbackToDestructiveMigration() - .addCallback(DatabaseCallback()) - .build() - } + @Provides + @Singleton + fun provideDatabase(@ApplicationContext context: Context): AppDatabase { + return Room.databaseBuilder( + context, + AppDatabase::class.java, + context.getString(R.string.db_name), + ) + .fallbackToDestructiveMigration() + .addCallback(DatabaseCallback()) + .build() + } - @Singleton - @Provides - fun provideSettingsDoa(appDatabase: AppDatabase): SettingsDao { - return appDatabase.settingDao() - } + @Singleton + @Provides + fun provideSettingsDoa(appDatabase: AppDatabase): SettingsDao { + return appDatabase.settingDao() + } - @Singleton - @Provides - fun provideTunnelConfigDoa(appDatabase: AppDatabase): TunnelConfigDao { - return appDatabase.tunnelConfigDoa() - } + @Singleton + @Provides + fun provideTunnelConfigDoa(appDatabase: AppDatabase): TunnelConfigDao { + return appDatabase.tunnelConfigDoa() + } - @Singleton - @Provides - fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao, @IoDispatcher ioDispatcher: CoroutineDispatcher): TunnelRepository { - return RoomTunnelRepository(tunnelConfigDao, ioDispatcher) - } + @Singleton + @Provides + fun provideTunnelConfigRepository( + tunnelConfigDao: TunnelConfigDao, + @IoDispatcher ioDispatcher: CoroutineDispatcher, + ): TunnelRepository { + return RoomTunnelRepository(tunnelConfigDao, ioDispatcher) + } - @Singleton - @Provides - fun provideSettingsRepository(settingsDao: SettingsDao, @IoDispatcher ioDispatcher: CoroutineDispatcher): AppSettingRepository { - return RoomSettingsRepository(settingsDao, ioDispatcher) - } + @Singleton + @Provides + fun provideSettingsRepository( + settingsDao: SettingsDao, + @IoDispatcher ioDispatcher: CoroutineDispatcher, + ): AppSettingRepository { + return RoomSettingsRepository(settingsDao, ioDispatcher) + } - @Singleton - @Provides - fun providePreferencesDataStore(@ApplicationContext context: Context, @IoDispatcher ioDispatcher: CoroutineDispatcher): DataStoreManager { - return DataStoreManager(context, ioDispatcher) - } + @Singleton + @Provides + fun providePreferencesDataStore( + @ApplicationContext context: Context, + @IoDispatcher ioDispatcher: CoroutineDispatcher, + ): DataStoreManager { + return DataStoreManager(context, ioDispatcher) + } - @Provides - @Singleton - fun provideGeneralStateRepository(dataStoreManager: DataStoreManager): AppStateRepository { - return DataStoreAppStateRepository(dataStoreManager) - } + @Provides + @Singleton + fun provideGeneralStateRepository(dataStoreManager: DataStoreManager): AppStateRepository { + return DataStoreAppStateRepository(dataStoreManager) + } - @Provides - @Singleton - fun provideAppDataRepository( - settingsRepository: AppSettingRepository, - tunnelRepository: TunnelRepository, - appStateRepository: AppStateRepository, - ): AppDataRepository { - return AppDataRoomRepository(settingsRepository, tunnelRepository, appStateRepository) - } + @Provides + @Singleton + fun provideAppDataRepository( + settingsRepository: AppSettingRepository, + tunnelRepository: TunnelRepository, + appStateRepository: AppStateRepository, + ): AppDataRepository { + return AppDataRoomRepository(settingsRepository, tunnelRepository, appStateRepository) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/TunnelModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/TunnelModule.kt index 5a96e27e..da2fd202 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/TunnelModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/TunnelModule.kt @@ -18,97 +18,121 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.runBlocking import org.amnezia.awg.backend.Backend import org.amnezia.awg.backend.GoBackend import org.amnezia.awg.backend.RootTunnelActionHandler -import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) class TunnelModule { - @Provides - @Singleton - @TunnelShell - fun provideTunnelRootShell(@ApplicationContext context: Context): RootShell { - return RootShell(context) - } + @Provides + @Singleton + @TunnelShell + fun provideTunnelRootShell(@ApplicationContext context: Context): RootShell { + return RootShell(context) + } - @Provides - @Singleton - @AppShell - fun provideAppRootShell(@ApplicationContext context: Context): RootShell { - return RootShell(context) - } + @Provides + @Singleton + @AppShell + fun provideAppRootShell(@ApplicationContext context: Context): RootShell { + return RootShell(context) + } - @Provides - @Singleton - fun provideAmneziaBackend(@ApplicationContext context: Context): Backend { - return GoBackend(context, RootTunnelActionHandler(org.amnezia.awg.util.RootShell(context))) - } + @Provides + @Singleton + fun provideAmneziaBackend(@ApplicationContext context: Context): Backend { + return GoBackend(context, RootTunnelActionHandler(org.amnezia.awg.util.RootShell(context))) + } - @Provides - @Singleton - fun provideKernelBackend(@ApplicationContext context: Context, @TunnelShell shell: RootShell): com.wireguard.android.backend.Backend { - return WgQuickBackend(context, shell, ToolsInstaller(context, shell), com.wireguard.android.backend.RootTunnelActionHandler(shell)).also { - it.setMultipleTunnels(true) - } - } + @Provides + @Singleton + fun provideKernelBackend( + @ApplicationContext context: Context, + @TunnelShell shell: RootShell, + ): com.wireguard.android.backend.Backend { + return WgQuickBackend( + context, + shell, + ToolsInstaller(context, shell), + com.wireguard.android.backend.RootTunnelActionHandler(shell), + ) + .also { it.setMultipleTunnels(true) } + } - @Provides - @Singleton - @Kernel - fun provideKernelProvider( - @ApplicationScope applicationScope: CoroutineScope, - serviceManager: ServiceManager, - appDataRepository: AppDataRepository, - backend: com.wireguard.android.backend.Backend, - ): TunnelProvider { - return KernelTunnel(applicationScope, serviceManager, appDataRepository, backend) - } + @Provides + @Singleton + @Kernel + fun provideKernelProvider( + @ApplicationScope applicationScope: CoroutineScope, + serviceManager: ServiceManager, + appDataRepository: AppDataRepository, + backend: com.wireguard.android.backend.Backend, + ): TunnelProvider { + return KernelTunnel(applicationScope, serviceManager, appDataRepository, backend) + } - @Provides - @Singleton - @Userspace - fun provideUserspaceProvider( - @ApplicationScope applicationScope: CoroutineScope, - serviceManager: ServiceManager, - appDataRepository: AppDataRepository, - backend: Backend, - ): TunnelProvider { - return UserspaceTunnel(applicationScope, serviceManager, appDataRepository, backend) - } + @Provides + @Singleton + @Userspace + fun provideUserspaceProvider( + @ApplicationScope applicationScope: CoroutineScope, + serviceManager: ServiceManager, + appDataRepository: AppDataRepository, + backend: Backend, + ): TunnelProvider { + return UserspaceTunnel(applicationScope, serviceManager, appDataRepository, backend) + } - @Provides - @Singleton - fun provideTunnelManager( - @Kernel kernelTunnel: TunnelProvider, - @Userspace userspaceTunnel: TunnelProvider, - appDataRepository: AppDataRepository, - @IoDispatcher ioDispatcher: CoroutineDispatcher, - @ApplicationScope applicationScope: CoroutineScope, - ): TunnelManager { - return TunnelManager(kernelTunnel, userspaceTunnel, appDataRepository, applicationScope, ioDispatcher) - } + @Provides + @Singleton + fun provideTunnelManager( + @Kernel kernelTunnel: TunnelProvider, + @Userspace userspaceTunnel: TunnelProvider, + appDataRepository: AppDataRepository, + @IoDispatcher ioDispatcher: CoroutineDispatcher, + @ApplicationScope applicationScope: CoroutineScope, + ): TunnelManager { + return TunnelManager( + kernelTunnel, + userspaceTunnel, + appDataRepository, + applicationScope, + ioDispatcher, + ) + } - @Provides - @Singleton - fun provideNetworkMonitor(@ApplicationContext context: Context, settingsRepository: AppSettingRepository): NetworkMonitor { - return AndroidNetworkMonitor(context) { runBlocking { settingsRepository.get().isWifiNameByShellEnabled } } - } + @Provides + @Singleton + fun provideNetworkMonitor( + @ApplicationContext context: Context, + settingsRepository: AppSettingRepository, + ): NetworkMonitor { + return AndroidNetworkMonitor(context) { + runBlocking { settingsRepository.get().isWifiNameByShellEnabled } + } + } - @Singleton - @Provides - fun provideServiceManager( - @ApplicationContext context: Context, - @IoDispatcher ioDispatcher: CoroutineDispatcher, - @MainDispatcher mainCoroutineDispatcher: CoroutineDispatcher, - @ApplicationScope applicationScope: CoroutineScope, - appDataRepository: AppDataRepository, - ): ServiceManager { - return ServiceManager(context, ioDispatcher, applicationScope, mainCoroutineDispatcher, appDataRepository) - } + @Singleton + @Provides + fun provideServiceManager( + @ApplicationContext context: Context, + @IoDispatcher ioDispatcher: CoroutineDispatcher, + @MainDispatcher mainCoroutineDispatcher: CoroutineDispatcher, + @ApplicationScope applicationScope: CoroutineScope, + appDataRepository: AppDataRepository, + ): ServiceManager { + return ServiceManager( + context, + ioDispatcher, + applicationScope, + mainCoroutineDispatcher, + appDataRepository, + ) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/ViewModelModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/ViewModelModule.kt index 414b2ec1..7086b894 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/ViewModelModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/ViewModelModule.kt @@ -13,9 +13,12 @@ import kotlinx.coroutines.CoroutineDispatcher @Module @InstallIn(ViewModelComponent::class) class ViewModelModule { - @ViewModelScoped - @Provides - fun provideFileUtils(@ApplicationContext context: Context, @IoDispatcher ioDispatcher: CoroutineDispatcher): FileUtils { - return FileUtils(context, ioDispatcher) - } + @ViewModelScoped + @Provides + fun provideFileUtils( + @ApplicationContext context: Context, + @IoDispatcher ioDispatcher: CoroutineDispatcher, + ): FileUtils { + return FileUtils(context, ioDispatcher) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/entity/AppSettings.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/entity/AppSettings.kt index 754e26dc..3879b718 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/entity/AppSettings.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/entity/AppSettings.kt @@ -1,29 +1,29 @@ package com.zaneschepke.wireguardautotunnel.domain.entity data class AppSettings( - val id: Int = 0, - val isAutoTunnelEnabled: Boolean = false, - val isTunnelOnMobileDataEnabled: Boolean = false, - val trustedNetworkSSIDs: List = emptyList(), - val isAlwaysOnVpnEnabled: Boolean = false, - val isTunnelOnEthernetEnabled: Boolean = false, - val isShortcutsEnabled: Boolean = false, - val isTunnelOnWifiEnabled: Boolean = false, - val isKernelEnabled: Boolean = false, - val isRestoreOnBootEnabled: Boolean = false, - val isMultiTunnelEnabled: Boolean = false, - val isPingEnabled: Boolean = false, - val isAmneziaEnabled: Boolean = false, - val isWildcardsEnabled: Boolean = false, - val isWifiNameByShellEnabled: Boolean = false, - val isStopOnNoInternetEnabled: Boolean = false, - val isVpnKillSwitchEnabled: Boolean = false, - val isKernelKillSwitchEnabled: Boolean = false, - val isLanOnKillSwitchEnabled: Boolean = false, - val debounceDelaySeconds: Int = 3, - val isDisableKillSwitchOnTrustedEnabled: Boolean = false, + val id: Int = 0, + val isAutoTunnelEnabled: Boolean = false, + val isTunnelOnMobileDataEnabled: Boolean = false, + val trustedNetworkSSIDs: List = emptyList(), + val isAlwaysOnVpnEnabled: Boolean = false, + val isTunnelOnEthernetEnabled: Boolean = false, + val isShortcutsEnabled: Boolean = false, + val isTunnelOnWifiEnabled: Boolean = false, + val isKernelEnabled: Boolean = false, + val isRestoreOnBootEnabled: Boolean = false, + val isMultiTunnelEnabled: Boolean = false, + val isPingEnabled: Boolean = false, + val isAmneziaEnabled: Boolean = false, + val isWildcardsEnabled: Boolean = false, + val isWifiNameByShellEnabled: Boolean = false, + val isStopOnNoInternetEnabled: Boolean = false, + val isVpnKillSwitchEnabled: Boolean = false, + val isKernelKillSwitchEnabled: Boolean = false, + val isLanOnKillSwitchEnabled: Boolean = false, + val debounceDelaySeconds: Int = 3, + val isDisableKillSwitchOnTrustedEnabled: Boolean = false, ) { - fun debounceDelayMillis(): Long { - return debounceDelaySeconds * 1000L - } + fun debounceDelayMillis(): Long { + return debounceDelaySeconds * 1000L + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/entity/AppState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/entity/AppState.kt index 6d3019fb..a9708255 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/entity/AppState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/entity/AppState.kt @@ -3,13 +3,13 @@ package com.zaneschepke.wireguardautotunnel.domain.entity import com.zaneschepke.wireguardautotunnel.ui.theme.Theme data class AppState( - val isLocationDisclosureShown: Boolean, - val isBatteryOptimizationDisableShown: Boolean, - val isPinLockEnabled: Boolean, - val isTunnelStatsExpanded: Boolean, - val isLocalLogsEnabled: Boolean, - val isRemoteControlEnabled: Boolean, - val remoteKey: String?, - val locale: String?, - val theme: Theme, + val isLocationDisclosureShown: Boolean, + val isBatteryOptimizationDisableShown: Boolean, + val isPinLockEnabled: Boolean, + val isTunnelStatsExpanded: Boolean, + val isLocalLogsEnabled: Boolean, + val isRemoteControlEnabled: Boolean, + val remoteKey: String?, + val locale: String?, + val theme: Theme, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/entity/TunnelConf.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/entity/TunnelConf.kt index 5836c1b9..4c265a63 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/entity/TunnelConf.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/entity/TunnelConf.kt @@ -8,177 +8,227 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.extractNameAndNumber import com.zaneschepke.wireguardautotunnel.util.extensions.hasNumberInParentheses import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString -import kotlinx.coroutines.withContext -import timber.log.Timber import java.io.InputStream import java.net.InetAddress import java.nio.charset.StandardCharsets import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.withContext +import timber.log.Timber data class TunnelConf( - val id: Int = 0, - val tunName: String, - val wgQuick: String, - val tunnelNetworks: List = emptyList(), - val isMobileDataTunnel: Boolean = false, - val isPrimaryTunnel: Boolean = false, - val amQuick: String, - val isActive: Boolean = false, - val isPingEnabled: Boolean = false, - val pingInterval: Long? = null, - val pingCooldown: Long? = null, - val pingIp: String? = null, - val isEthernetTunnel: Boolean = false, - val isIpv4Preferred: Boolean = true, - @Transient - private var stateChangeCallback: ((Any) -> Unit)? = null, + val id: Int = 0, + val tunName: String, + val wgQuick: String, + val tunnelNetworks: List = emptyList(), + val isMobileDataTunnel: Boolean = false, + val isPrimaryTunnel: Boolean = false, + val amQuick: String, + val isActive: Boolean = false, + val isPingEnabled: Boolean = false, + val pingInterval: Long? = null, + val pingCooldown: Long? = null, + val pingIp: String? = null, + val isEthernetTunnel: Boolean = false, + val isIpv4Preferred: Boolean = true, + @Transient private var stateChangeCallback: ((Any) -> Unit)? = null, ) : Tunnel, org.amnezia.awg.backend.Tunnel { - fun setStateChangeCallback(callback: (Any) -> Unit) { - stateChangeCallback = callback - } + fun setStateChangeCallback(callback: (Any) -> Unit) { + stateChangeCallback = callback + } - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is TunnelConf) return false - return id == other.id && tunName == other.tunName && wgQuick == other.wgQuick && amQuick == other.amQuick && - isPrimaryTunnel == other.isPrimaryTunnel && isMobileDataTunnel == other.isMobileDataTunnel && - isEthernetTunnel == other.isEthernetTunnel && isPingEnabled == other.isPingEnabled && pingIp == other.pingIp && - pingCooldown == other.pingCooldown && pingInterval == other.pingInterval && tunnelNetworks == other.tunnelNetworks && - isIpv4Preferred == other.isIpv4Preferred - } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is TunnelConf) return false + return id == other.id && + tunName == other.tunName && + wgQuick == other.wgQuick && + amQuick == other.amQuick && + isPrimaryTunnel == other.isPrimaryTunnel && + isMobileDataTunnel == other.isMobileDataTunnel && + isEthernetTunnel == other.isEthernetTunnel && + isPingEnabled == other.isPingEnabled && + pingIp == other.pingIp && + pingCooldown == other.pingCooldown && + pingInterval == other.pingInterval && + tunnelNetworks == other.tunnelNetworks && + isIpv4Preferred == other.isIpv4Preferred + } - override fun hashCode(): Int { - var result = id - result = 31 * result + tunName.hashCode() - result = 31 * result + wgQuick.hashCode() - result = 31 * result + amQuick.hashCode() - return result - } + override fun hashCode(): Int { + var result = id + result = 31 * result + tunName.hashCode() + result = 31 * result + wgQuick.hashCode() + result = 31 * result + amQuick.hashCode() + return result + } - fun copyWithCallback( - id: Int = this.id, - tunName: String = this.tunName, - wgQuick: String = this.wgQuick, - tunnelNetworks: List = this.tunnelNetworks, - isMobileDataTunnel: Boolean = this.isMobileDataTunnel, - isPrimaryTunnel: Boolean = this.isPrimaryTunnel, - amQuick: String = this.amQuick, - isActive: Boolean = this.isActive, - isPingEnabled: Boolean = this.isPingEnabled, - pingInterval: Long? = this.pingInterval, - pingCooldown: Long? = this.pingCooldown, - pingIp: String? = this.pingIp, - isEthernetTunnel: Boolean = this.isEthernetTunnel, - isIpv4Preferred: Boolean = this.isIpv4Preferred, - ): TunnelConf { - return TunnelConf( - id, tunName, wgQuick, tunnelNetworks, isMobileDataTunnel, isPrimaryTunnel, - amQuick, isActive, isPingEnabled, pingInterval, pingCooldown, pingIp, - isEthernetTunnel, isIpv4Preferred, - ).apply { - stateChangeCallback = this@TunnelConf.stateChangeCallback -// tunnelStatsCallback = this@TunnelConf.tunnelStatsCallback -// bounceTunnelCallback = this@TunnelConf.bounceTunnelCallback - } - } + fun copyWithCallback( + id: Int = this.id, + tunName: String = this.tunName, + wgQuick: String = this.wgQuick, + tunnelNetworks: List = this.tunnelNetworks, + isMobileDataTunnel: Boolean = this.isMobileDataTunnel, + isPrimaryTunnel: Boolean = this.isPrimaryTunnel, + amQuick: String = this.amQuick, + isActive: Boolean = this.isActive, + isPingEnabled: Boolean = this.isPingEnabled, + pingInterval: Long? = this.pingInterval, + pingCooldown: Long? = this.pingCooldown, + pingIp: String? = this.pingIp, + isEthernetTunnel: Boolean = this.isEthernetTunnel, + isIpv4Preferred: Boolean = this.isIpv4Preferred, + ): TunnelConf { + return TunnelConf( + id, + tunName, + wgQuick, + tunnelNetworks, + isMobileDataTunnel, + isPrimaryTunnel, + amQuick, + isActive, + isPingEnabled, + pingInterval, + pingCooldown, + pingIp, + isEthernetTunnel, + isIpv4Preferred, + ) + .apply { + stateChangeCallback = this@TunnelConf.stateChangeCallback + // tunnelStatsCallback = this@TunnelConf.tunnelStatsCallback + // bounceTunnelCallback = this@TunnelConf.bounceTunnelCallback + } + } -// fun onUpdateStatistics() { -// tunnelStatsCallback?.invoke() -// } -// -// fun bounceTunnel(tunnelConf: TunnelConf, reason: TunnelStatus.StopReason) { -// bounceTunnelCallback?.invoke(tunnelConf, reason) -// } + // fun onUpdateStatistics() { + // tunnelStatsCallback?.invoke() + // } + // + // fun bounceTunnel(tunnelConf: TunnelConf, reason: TunnelStatus.StopReason) { + // bounceTunnelCallback?.invoke(tunnelConf, reason) + // } - fun toAmConfig(): org.amnezia.awg.config.Config { - return configFromAmQuick(amQuick.ifBlank { wgQuick }) - } + fun toAmConfig(): org.amnezia.awg.config.Config { + return configFromAmQuick(amQuick.ifBlank { wgQuick }) + } - fun toWgConfig(): Config { - return configFromWgQuick(wgQuick) - } + fun toWgConfig(): Config { + return configFromWgQuick(wgQuick) + } - override fun getName(): String = tunName + override fun getName(): String = tunName - override fun isIpv4ResolutionPreferred(): Boolean = isIpv4Preferred + override fun isIpv4ResolutionPreferred(): Boolean = isIpv4Preferred - override fun onStateChange(newState: org.amnezia.awg.backend.Tunnel.State) { - stateChangeCallback?.invoke(newState) - } + override fun onStateChange(newState: org.amnezia.awg.backend.Tunnel.State) { + stateChangeCallback?.invoke(newState) + } - override fun onStateChange(newState: Tunnel.State) { - stateChangeCallback?.invoke(newState) - } + override fun onStateChange(newState: Tunnel.State) { + stateChangeCallback?.invoke(newState) + } - fun isTunnelConfigChanged(updatedConf: TunnelConf): Boolean { - return updatedConf.wgQuick != wgQuick || updatedConf.amQuick != amQuick || updatedConf.name != name - } + fun isTunnelConfigChanged(updatedConf: TunnelConf): Boolean { + return updatedConf.wgQuick != wgQuick || + updatedConf.amQuick != amQuick || + updatedConf.name != name + } - fun generateUniqueName(tunnelNames: List): String { - var tunnelName = this.tunName - var num = 1 - while (tunnelNames.any { it == tunnelName }) { - tunnelName = if (!tunnelName.hasNumberInParentheses()) { - "$name($num)" - } else { - val pair = tunnelName.extractNameAndNumber() - "${pair?.first}($num)" - } - num++ - } - return tunnelName - } + fun generateUniqueName(tunnelNames: List): String { + var tunnelName = this.tunName + var num = 1 + while (tunnelNames.any { it == tunnelName }) { + tunnelName = + if (!tunnelName.hasNumberInParentheses()) { + "$name($num)" + } else { + val pair = tunnelName.extractNameAndNumber() + "${pair?.first}($num)" + } + num++ + } + return tunnelName + } - suspend fun isTunnelPingable(context: CoroutineContext): Boolean { - return withContext(context) { - val config = toWgConfig() - if (pingIp != null) { - return@withContext InetAddress.getByName(pingIp) - .isReachable(Constants.PING_TIMEOUT.toInt()).also { - Timber.i("Ping reachable $pingIp: $it") - } - } - config.peers.map { peer -> - peer.isReachable() - }.all { true }.also { - Timber.i("Ping of all peers reachable: $it") - } - } - } + suspend fun isTunnelPingable(context: CoroutineContext): Boolean { + return withContext(context) { + val config = toWgConfig() + if (pingIp != null) { + return@withContext InetAddress.getByName(pingIp) + .isReachable(Constants.PING_TIMEOUT.toInt()) + .also { Timber.i("Ping reachable $pingIp: $it") } + } + config.peers + .map { peer -> peer.isReachable() } + .all { true } + .also { Timber.i("Ping of all peers reachable: $it") } + } + } - companion object { - fun configFromWgQuick(wgQuick: String): Config { - val inputStream: InputStream = wgQuick.byteInputStream() - return inputStream.bufferedReader(StandardCharsets.UTF_8).use { - Config.parse(it) - } - } + companion object { + fun configFromWgQuick(wgQuick: String): Config { + val inputStream: InputStream = wgQuick.byteInputStream() + return inputStream.bufferedReader(StandardCharsets.UTF_8).use { Config.parse(it) } + } - fun configFromAmQuick(amQuick: String): org.amnezia.awg.config.Config { - val inputStream: InputStream = amQuick.byteInputStream() - return inputStream.bufferedReader(StandardCharsets.UTF_8).use { - org.amnezia.awg.config.Config.parse(it) - } - } + fun configFromAmQuick(amQuick: String): org.amnezia.awg.config.Config { + val inputStream: InputStream = amQuick.byteInputStream() + return inputStream.bufferedReader(StandardCharsets.UTF_8).use { + org.amnezia.awg.config.Config.parse(it) + } + } - fun tunnelConfigFromAmConfig(config: org.amnezia.awg.config.Config, name: String? = null): TunnelConf { - val amQuick = config.toAwgQuickString(true) - val wgQuick = config.toWgQuickString() - return TunnelConf(tunName = name ?: config.defaultName(), wgQuick = wgQuick, amQuick = amQuick) - } + fun tunnelConfigFromAmConfig( + config: org.amnezia.awg.config.Config, + name: String? = null, + ): TunnelConf { + val amQuick = config.toAwgQuickString(true) + val wgQuick = config.toWgQuickString() + return TunnelConf( + tunName = name ?: config.defaultName(), + wgQuick = wgQuick, + amQuick = amQuick, + ) + } - private const val IPV6_ALL_NETWORKS = "::/0" - private const val IPV4_ALL_NETWORKS = "0.0.0.0/0" - val ALL_IPS = listOf(IPV4_ALL_NETWORKS, IPV6_ALL_NETWORKS) - private val IPV4_PUBLIC_NETWORKS = listOf( - "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 LAN_BYPASS_ALLOWED_IPS = listOf(IPV6_ALL_NETWORKS) + IPV4_PUBLIC_NETWORKS - } + private const val IPV6_ALL_NETWORKS = "::/0" + private const val IPV4_ALL_NETWORKS = "0.0.0.0/0" + val ALL_IPS = listOf(IPV4_ALL_NETWORKS, IPV6_ALL_NETWORKS) + private val IPV4_PUBLIC_NETWORKS = + listOf( + "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 LAN_BYPASS_ALLOWED_IPS = listOf(IPV6_ALL_NETWORKS) + IPV4_PUBLIC_NETWORKS + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/BackendError.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/BackendError.kt index 8ca7c554..657bb116 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/BackendError.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/BackendError.kt @@ -3,22 +3,31 @@ package com.zaneschepke.wireguardautotunnel.domain.enums import com.zaneschepke.wireguardautotunnel.R sealed class BackendError : Exception() { - data object DNS : BackendError() - data object Unauthorized : BackendError() - data object Config : BackendError() - data object KernelModuleName : BackendError() - data object InvalidConfig : BackendError() - data object NotAuthorized : BackendError() - data object ServiceNotRunning : BackendError() - data object Unknown : BackendError() + data object DNS : BackendError() - fun toStringRes() = when (this) { - Config -> R.string.config_error - DNS -> R.string.dns_resolve_error - InvalidConfig -> R.string.invalid_config_error - KernelModuleName -> R.string.kernel_name_error - NotAuthorized, Unauthorized -> R.string.auth_error - ServiceNotRunning -> R.string.service_running_error - Unknown -> R.string.unknown_error - } + data object Unauthorized : BackendError() + + data object Config : BackendError() + + data object KernelModuleName : BackendError() + + data object InvalidConfig : BackendError() + + data object NotAuthorized : BackendError() + + data object ServiceNotRunning : BackendError() + + data object Unknown : BackendError() + + fun toStringRes() = + when (this) { + Config -> R.string.config_error + DNS -> R.string.dns_resolve_error + InvalidConfig -> R.string.invalid_config_error + KernelModuleName -> R.string.kernel_name_error + NotAuthorized, + Unauthorized -> R.string.auth_error + ServiceNotRunning -> R.string.service_running_error + Unknown -> R.string.unknown_error + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/BackendState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/BackendState.kt index 94494b29..a29d60a6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/BackendState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/BackendState.kt @@ -1,7 +1,7 @@ package com.zaneschepke.wireguardautotunnel.domain.enums enum class BackendState { - KILL_SWITCH_ACTIVE, - SERVICE_ACTIVE, - INACTIVE, + KILL_SWITCH_ACTIVE, + SERVICE_ACTIVE, + INACTIVE, } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/ConfigType.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/ConfigType.kt index d7f05f9f..8ca980f0 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/ConfigType.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/ConfigType.kt @@ -1,6 +1,6 @@ package com.zaneschepke.wireguardautotunnel.domain.enums enum class ConfigType { - AMNEZIA, - WG, + AMNEZIA, + WG, } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/HandshakeStatus.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/HandshakeStatus.kt index 71cb53eb..c894f9b2 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/HandshakeStatus.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/HandshakeStatus.kt @@ -1,17 +1,16 @@ package com.zaneschepke.wireguardautotunnel.domain.enums enum class HandshakeStatus { - HEALTHY, - STALE, - UNKNOWN, - NOT_STARTED, - ; + HEALTHY, + STALE, + UNKNOWN, + 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 NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC = 30 - } + 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 NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC = 30 + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/NotificationAction.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/NotificationAction.kt index 2a48f779..3a8a9c4d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/NotificationAction.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/NotificationAction.kt @@ -4,14 +4,13 @@ import android.content.Context import com.zaneschepke.wireguardautotunnel.R enum class NotificationAction { - TUNNEL_OFF, - AUTO_TUNNEL_OFF, - ; + TUNNEL_OFF, + AUTO_TUNNEL_OFF; - fun title(context: Context): String { - return when (this) { - TUNNEL_OFF -> context.getString(R.string.stop) - AUTO_TUNNEL_OFF -> context.getString(R.string.stop) - } - } + fun title(context: Context): String { + return when (this) { + TUNNEL_OFF -> context.getString(R.string.stop) + AUTO_TUNNEL_OFF -> context.getString(R.string.stop) + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/TunnelStatus.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/TunnelStatus.kt index f4ecb291..caf5c768 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/TunnelStatus.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/TunnelStatus.kt @@ -1,31 +1,35 @@ package com.zaneschepke.wireguardautotunnel.domain.enums sealed class TunnelStatus { - data class Error(val error: BackendError) : TunnelStatus() - data object Up : TunnelStatus() - data object Down : TunnelStatus() - data class Stopping(val reason: StopReason) : TunnelStatus() - data object Starting : TunnelStatus() + data class Error(val error: BackendError) : TunnelStatus() - enum class StopReason { - USER, - PING, - CONFIG_CHANGED, - } + data object Up : TunnelStatus() - fun isDown(): Boolean { - return this == Down - } + data object Down : TunnelStatus() - fun isUp(): Boolean { - return this == Up - } + data class Stopping(val reason: StopReason) : TunnelStatus() - fun isUpOrStarting(): Boolean { - return this == Up || this == Starting - } + data object Starting : TunnelStatus() - fun isDownOrStopping(): Boolean { - return this == Down || this is Stopping - } + enum class StopReason { + USER, + PING, + CONFIG_CHANGED, + } + + fun isDown(): Boolean { + return this == Down + } + + fun isUp(): Boolean { + return this == Up + } + + fun isUpOrStarting(): Boolean { + return this == Up || this == Starting + } + + fun isDownOrStopping(): Boolean { + return this == Down || this is Stopping + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/events/AutoTunnelEvent.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/events/AutoTunnelEvent.kt index 7ae73471..948308d3 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/events/AutoTunnelEvent.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/events/AutoTunnelEvent.kt @@ -3,7 +3,9 @@ package com.zaneschepke.wireguardautotunnel.domain.events import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf sealed class AutoTunnelEvent { - data class Start(val tunnelConf: TunnelConf? = null) : AutoTunnelEvent() - data object Stop : AutoTunnelEvent() - data object DoNothing : AutoTunnelEvent() + data class Start(val tunnelConf: TunnelConf? = null) : AutoTunnelEvent() + + data object Stop : AutoTunnelEvent() + + data object DoNothing : AutoTunnelEvent() } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/events/KillSwitchEvent.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/events/KillSwitchEvent.kt index 5b751ab6..137d1a57 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/events/KillSwitchEvent.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/events/KillSwitchEvent.kt @@ -1,7 +1,9 @@ package com.zaneschepke.wireguardautotunnel.domain.events sealed class KillSwitchEvent { - data class Start(val allowedIps: List) : KillSwitchEvent() - data object Stop : KillSwitchEvent() - data object DoNothing : KillSwitchEvent() + data class Start(val allowedIps: List) : KillSwitchEvent() + + data object Stop : KillSwitchEvent() + + data object DoNothing : KillSwitchEvent() } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/repository/AppDataRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/repository/AppDataRepository.kt index 9db6c170..ec2d83ea 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/repository/AppDataRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/repository/AppDataRepository.kt @@ -3,11 +3,11 @@ package com.zaneschepke.wireguardautotunnel.domain.repository import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf interface AppDataRepository { - suspend fun getPrimaryOrFirstTunnel(): TunnelConf? + suspend fun getPrimaryOrFirstTunnel(): TunnelConf? - suspend fun getStartTunnelConfig(): TunnelConf? + suspend fun getStartTunnelConfig(): TunnelConf? - val settings: AppSettingRepository - val tunnels: TunnelRepository - val appState: AppStateRepository + val settings: AppSettingRepository + val tunnels: TunnelRepository + val appState: AppStateRepository } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/repository/AppSettingRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/repository/AppSettingRepository.kt index 7ea1eb77..d75c558b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/repository/AppSettingRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/repository/AppSettingRepository.kt @@ -4,7 +4,9 @@ import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings import kotlinx.coroutines.flow.Flow interface AppSettingRepository { - suspend fun save(appSettings: AppSettings) - val flow: Flow - suspend fun get(): AppSettings + suspend fun save(appSettings: AppSettings) + + val flow: Flow + + suspend fun get(): AppSettings } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/repository/AppStateRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/repository/AppStateRepository.kt index 094b175d..27cce322 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/repository/AppStateRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/repository/AppStateRepository.kt @@ -5,41 +5,41 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.Theme import kotlinx.coroutines.flow.Flow interface AppStateRepository { - suspend fun isLocationDisclosureShown(): Boolean + suspend fun isLocationDisclosureShown(): Boolean - suspend fun setLocationDisclosureShown(shown: Boolean) + suspend fun setLocationDisclosureShown(shown: Boolean) - suspend fun isPinLockEnabled(): Boolean + suspend fun isPinLockEnabled(): Boolean - suspend fun setPinLockEnabled(enabled: Boolean) + suspend fun setPinLockEnabled(enabled: Boolean) - suspend fun isBatteryOptimizationDisableShown(): Boolean + suspend fun isBatteryOptimizationDisableShown(): Boolean - suspend fun setBatteryOptimizationDisableShown(shown: Boolean) + suspend fun setBatteryOptimizationDisableShown(shown: Boolean) - suspend fun isTunnelStatsExpanded(): Boolean + suspend fun isTunnelStatsExpanded(): Boolean - suspend fun setTunnelStatsExpanded(expanded: Boolean) + suspend fun setTunnelStatsExpanded(expanded: Boolean) - suspend fun setTheme(theme: Theme) + suspend fun setTheme(theme: Theme) - suspend fun getTheme(): Theme + suspend fun getTheme(): Theme - suspend fun isLocalLogsEnabled(): Boolean + suspend fun isLocalLogsEnabled(): Boolean - suspend fun setLocalLogsEnabled(enabled: Boolean) + suspend fun setLocalLogsEnabled(enabled: Boolean) - suspend fun setLocale(localeTag: String) + suspend fun setLocale(localeTag: String) - suspend fun getLocale(): String? + suspend fun getLocale(): String? - suspend fun setIsRemoteControlEnabled(enabled: Boolean) + suspend fun setIsRemoteControlEnabled(enabled: Boolean) - suspend fun isRemoteControlEnabled(): Boolean + suspend fun isRemoteControlEnabled(): Boolean - suspend fun setRemoteKey(key: String) + suspend fun setRemoteKey(key: String) - suspend fun getRemoteKey(): String? + suspend fun getRemoteKey(): String? - val flow: Flow + val flow: Flow } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/repository/TunnelRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/repository/TunnelRepository.kt index 83991343..55c8a38e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/repository/TunnelRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/repository/TunnelRepository.kt @@ -5,33 +5,33 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels import kotlinx.coroutines.flow.Flow interface TunnelRepository { - val flow: Flow> + val flow: Flow> - suspend fun getAll(): Tunnels + suspend fun getAll(): Tunnels - suspend fun save(tunnelConf: TunnelConf) + suspend fun save(tunnelConf: TunnelConf) - suspend fun saveAll(tunnelConfList: List) + suspend fun saveAll(tunnelConfList: List) - suspend fun updatePrimaryTunnel(tunnelConf: TunnelConf?) + suspend fun updatePrimaryTunnel(tunnelConf: TunnelConf?) - suspend fun updateMobileDataTunnel(tunnelConf: TunnelConf?) + suspend fun updateMobileDataTunnel(tunnelConf: TunnelConf?) - suspend fun updateEthernetTunnel(tunnelConf: TunnelConf?) + suspend fun updateEthernetTunnel(tunnelConf: TunnelConf?) - suspend fun delete(tunnelConf: TunnelConf) + suspend fun delete(tunnelConf: TunnelConf) - suspend fun getById(id: Int): TunnelConf? + suspend fun getById(id: Int): TunnelConf? - suspend fun getActive(): Tunnels + suspend fun getActive(): Tunnels - suspend fun count(): Int + suspend fun count(): Int - suspend fun findByTunnelName(name: String): TunnelConf? + suspend fun findByTunnelName(name: String): TunnelConf? - suspend fun findByTunnelNetworksName(name: String): Tunnels + suspend fun findByTunnelNetworksName(name: String): Tunnels - suspend fun findByMobileDataTunnel(): Tunnels + suspend fun findByMobileDataTunnel(): Tunnels - suspend fun findPrimary(): Tunnels + suspend fun findPrimary(): Tunnels } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/AmneziaStatistics.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/AmneziaStatistics.kt index 8bf01995..c959de6b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/AmneziaStatistics.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/AmneziaStatistics.kt @@ -4,31 +4,31 @@ import org.amnezia.awg.backend.Statistics import org.amnezia.awg.crypto.Key class AmneziaStatistics(private val statistics: Statistics) : TunnelStatistics() { - override fun peerStats(peer: Key): PeerStats? { - val key = Key.fromBase64(peer.toBase64()) - val stats = statistics.peer(key) - return stats?.let { - PeerStats( - rxBytes = stats.rxBytes, - txBytes = stats.txBytes, - latestHandshakeEpochMillis = stats.latestHandshakeEpochMillis, - ) - } - } + override fun peerStats(peer: Key): PeerStats? { + val key = Key.fromBase64(peer.toBase64()) + val stats = statistics.peer(key) + return stats?.let { + PeerStats( + rxBytes = stats.rxBytes, + txBytes = stats.txBytes, + latestHandshakeEpochMillis = stats.latestHandshakeEpochMillis, + ) + } + } - override fun isTunnelStale(): Boolean { - return statistics.isStale - } + override fun isTunnelStale(): Boolean { + return statistics.isStale + } - override fun getPeers(): Array { - return statistics.peers() - } + override fun getPeers(): Array { + return statistics.peers() + } - override fun rx(): Long { - return statistics.totalRx() - } + override fun rx(): Long { + return statistics.totalRx() + } - override fun tx(): Long { - return statistics.totalTx() - } + override fun tx(): Long { + return statistics.totalTx() + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/AutoTunnelState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/AutoTunnelState.kt index f73871b3..a6ebfdd5 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/AutoTunnelState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/AutoTunnelState.kt @@ -10,150 +10,189 @@ import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList data class AutoTunnelState( - val activeTunnels: Map = emptyMap(), - val networkState: NetworkState = NetworkState(), - val settings: AppSettings = AppSettings(), - val tunnels: List = emptyList(), + val activeTunnels: Map = emptyMap(), + val networkState: NetworkState = NetworkState(), + val settings: AppSettings = AppSettings(), + val tunnels: List = emptyList(), ) { - private fun isMobileDataActive(): Boolean { - return !networkState.isEthernetConnected && !networkState.isWifiConnected && networkState.isMobileDataConnected - } + private fun isMobileDataActive(): Boolean { + return !networkState.isEthernetConnected && + !networkState.isWifiConnected && + networkState.isMobileDataConnected + } - private fun isMobileTunnelDataChangeNeeded(): Boolean { - val preferredTunnel = preferredMobileDataTunnel() - return preferredTunnel != null && - activeTunnels.isNotEmpty() && !activeTunnels.isUp(preferredTunnel) - } + private fun isMobileTunnelDataChangeNeeded(): Boolean { + val preferredTunnel = preferredMobileDataTunnel() + return preferredTunnel != null && + activeTunnels.isNotEmpty() && + !activeTunnels.isUp(preferredTunnel) + } - private fun isEthernetTunnelChangeNeeded(): Boolean { - val preferredTunnel = preferredEthernetTunnel() - return preferredTunnel != null && activeTunnels.isNotEmpty() && !activeTunnels.isUp(preferredTunnel) - } + private fun isEthernetTunnelChangeNeeded(): Boolean { + val preferredTunnel = preferredEthernetTunnel() + return preferredTunnel != null && + activeTunnels.isNotEmpty() && + !activeTunnels.isUp(preferredTunnel) + } - private fun preferredMobileDataTunnel(): TunnelConf? { - return tunnels.firstOrNull { it.isMobileDataTunnel } ?: tunnels.firstOrNull { it.isPrimaryTunnel } - } + private fun preferredMobileDataTunnel(): TunnelConf? { + return tunnels.firstOrNull { it.isMobileDataTunnel } + ?: tunnels.firstOrNull { it.isPrimaryTunnel } + } - private fun preferredEthernetTunnel(): TunnelConf? { - return tunnels.firstOrNull { it.isEthernetTunnel } ?: tunnels.firstOrNull { it.isPrimaryTunnel } - } + private fun preferredEthernetTunnel(): TunnelConf? { + return tunnels.firstOrNull { it.isEthernetTunnel } + ?: tunnels.firstOrNull { it.isPrimaryTunnel } + } - private fun preferredWifiTunnel(): TunnelConf? { - return getTunnelWithMatchingTunnelNetwork() ?: tunnels.firstOrNull { it.isPrimaryTunnel } - } + private fun preferredWifiTunnel(): TunnelConf? { + return getTunnelWithMatchingTunnelNetwork() ?: tunnels.firstOrNull { it.isPrimaryTunnel } + } - private fun isWifiActive(): Boolean { - return !networkState.isEthernetConnected && networkState.isWifiConnected - } + private fun isWifiActive(): Boolean { + return !networkState.isEthernetConnected && networkState.isWifiConnected + } - private fun startOnEthernet(): Boolean { - return networkState.isEthernetConnected && settings.isTunnelOnEthernetEnabled && activeTunnels.allDown() - } + private fun startOnEthernet(): Boolean { + return networkState.isEthernetConnected && + settings.isTunnelOnEthernetEnabled && + activeTunnels.allDown() + } - private fun stopOnEthernet(): Boolean { - return networkState.isEthernetConnected && !settings.isTunnelOnEthernetEnabled && activeTunnels.hasActive() - } + private fun stopOnEthernet(): Boolean { + return networkState.isEthernetConnected && + !settings.isTunnelOnEthernetEnabled && + activeTunnels.hasActive() + } - // TODO test removed kill switch state check - private fun stopKillSwitchOnTrusted(): Boolean { - return networkState.isWifiConnected && settings.isVpnKillSwitchEnabled && settings.isDisableKillSwitchOnTrustedEnabled && isCurrentSSIDTrusted() - } + // TODO test removed kill switch state check + private fun stopKillSwitchOnTrusted(): Boolean { + return networkState.isWifiConnected && + settings.isVpnKillSwitchEnabled && + settings.isDisableKillSwitchOnTrustedEnabled && + isCurrentSSIDTrusted() + } - // TODO test, removed kill switch state check - private fun startKillSwitch(): Boolean { - return settings.isVpnKillSwitchEnabled && (!settings.isDisableKillSwitchOnTrustedEnabled || !isCurrentSSIDTrusted()) - } + // TODO test, removed kill switch state check + private fun startKillSwitch(): Boolean { + return settings.isVpnKillSwitchEnabled && + (!settings.isDisableKillSwitchOnTrustedEnabled || !isCurrentSSIDTrusted()) + } - private fun isNoConnectivity(): Boolean { - return !networkState.isEthernetConnected && !networkState.isWifiConnected && !networkState.isMobileDataConnected - } + private fun isNoConnectivity(): Boolean { + return !networkState.isEthernetConnected && + !networkState.isWifiConnected && + !networkState.isMobileDataConnected + } - private fun stopOnMobileData(): Boolean { - return isMobileDataActive() && !settings.isTunnelOnMobileDataEnabled && activeTunnels.hasActive() - } + private fun stopOnMobileData(): Boolean { + return isMobileDataActive() && + !settings.isTunnelOnMobileDataEnabled && + activeTunnels.hasActive() + } - private fun startOnMobileData(): Boolean { - return isMobileDataActive() && settings.isTunnelOnMobileDataEnabled && activeTunnels.allDown() - } + private fun startOnMobileData(): Boolean { + return isMobileDataActive() && + settings.isTunnelOnMobileDataEnabled && + activeTunnels.allDown() + } - private fun changeOnMobileData(): Boolean { - return isMobileDataActive() && settings.isTunnelOnMobileDataEnabled && isMobileTunnelDataChangeNeeded() - } + private fun changeOnMobileData(): Boolean { + return isMobileDataActive() && + settings.isTunnelOnMobileDataEnabled && + isMobileTunnelDataChangeNeeded() + } - private fun changeOnEthernet(): Boolean { - return networkState.isEthernetConnected && settings.isTunnelOnEthernetEnabled && isEthernetTunnelChangeNeeded() - } + private fun changeOnEthernet(): Boolean { + return networkState.isEthernetConnected && + settings.isTunnelOnEthernetEnabled && + isEthernetTunnelChangeNeeded() + } - private fun stopOnWifi(): Boolean { - return isWifiActive() && !settings.isTunnelOnWifiEnabled && activeTunnels.hasActive() - } + private fun stopOnWifi(): Boolean { + return isWifiActive() && !settings.isTunnelOnWifiEnabled && activeTunnels.hasActive() + } - private fun stopOnTrustedWifi(): Boolean { - return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.hasActive() && isCurrentSSIDTrusted() - } + private fun stopOnTrustedWifi(): Boolean { + return isWifiActive() && + settings.isTunnelOnWifiEnabled && + activeTunnels.hasActive() && + isCurrentSSIDTrusted() + } - private fun startOnUntrustedWifi(): Boolean { - return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.allDown() && !isCurrentSSIDTrusted() - } + private fun startOnUntrustedWifi(): Boolean { + return isWifiActive() && + settings.isTunnelOnWifiEnabled && + activeTunnels.allDown() && + !isCurrentSSIDTrusted() + } - private fun changeOnUntrustedWifi(): Boolean { - return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.hasActive() && !isCurrentSSIDTrusted() && !isWifiTunnelPreferred() - } + private fun changeOnUntrustedWifi(): Boolean { + return isWifiActive() && + settings.isTunnelOnWifiEnabled && + activeTunnels.hasActive() && + !isCurrentSSIDTrusted() && + !isWifiTunnelPreferred() + } - private fun isWifiTunnelPreferred(): Boolean { - val preferred = preferredWifiTunnel() - return preferred?.let { activeTunnels.isUp(it) } ?: true - } + private fun isWifiTunnelPreferred(): Boolean { + val preferred = preferredWifiTunnel() + return preferred?.let { activeTunnels.isUp(it) } ?: true + } - fun asAutoTunnelEvent(): AutoTunnelEvent { - return when { - // ethernet scenarios - stopOnEthernet() -> AutoTunnelEvent.Stop - startOnEthernet() || changeOnEthernet() -> AutoTunnelEvent.Start(preferredEthernetTunnel()) - // mobile data scenarios - stopOnMobileData() -> AutoTunnelEvent.Stop - startOnMobileData() || changeOnMobileData() -> AutoTunnelEvent.Start(preferredMobileDataTunnel()) - // wifi scenarios - stopOnWifi() -> AutoTunnelEvent.Stop - stopOnTrustedWifi() -> AutoTunnelEvent.Stop - startOnUntrustedWifi() || changeOnUntrustedWifi() -> AutoTunnelEvent.Start(preferredWifiTunnel()) - // no connectivity - isNoConnectivity() && settings.isStopOnNoInternetEnabled -> AutoTunnelEvent.Stop - else -> AutoTunnelEvent.DoNothing - } - } + fun asAutoTunnelEvent(): AutoTunnelEvent { + return when { + // ethernet scenarios + stopOnEthernet() -> AutoTunnelEvent.Stop + startOnEthernet() || changeOnEthernet() -> + AutoTunnelEvent.Start(preferredEthernetTunnel()) + // mobile data scenarios + stopOnMobileData() -> AutoTunnelEvent.Stop + startOnMobileData() || changeOnMobileData() -> + AutoTunnelEvent.Start(preferredMobileDataTunnel()) + // wifi scenarios + stopOnWifi() -> AutoTunnelEvent.Stop + stopOnTrustedWifi() -> AutoTunnelEvent.Stop + startOnUntrustedWifi() || changeOnUntrustedWifi() -> + AutoTunnelEvent.Start(preferredWifiTunnel()) + // no connectivity + isNoConnectivity() && settings.isStopOnNoInternetEnabled -> AutoTunnelEvent.Stop + else -> AutoTunnelEvent.DoNothing + } + } - fun asKillSwitchEvent(): KillSwitchEvent { - return when { - stopKillSwitchOnTrusted() -> KillSwitchEvent.Stop - startKillSwitch() -> { - val allowedIps = if (settings.isLanOnKillSwitchEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS else emptyList() - KillSwitchEvent.Start(allowedIps) - } - else -> KillSwitchEvent.DoNothing - } - } + fun asKillSwitchEvent(): KillSwitchEvent { + return when { + stopKillSwitchOnTrusted() -> KillSwitchEvent.Stop + startKillSwitch() -> { + val allowedIps = + if (settings.isLanOnKillSwitchEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS + else emptyList() + KillSwitchEvent.Start(allowedIps) + } + else -> KillSwitchEvent.DoNothing + } + } - private fun isCurrentSSIDTrusted(): Boolean { - return networkState.wifiName?.let { - hasTrustedWifiName(it) - } == true - } + private fun isCurrentSSIDTrusted(): Boolean { + return networkState.wifiName?.let { hasTrustedWifiName(it) } == true + } - private fun hasTrustedWifiName(wifiName: String, wifiNames: List = settings.trustedNetworkSSIDs): Boolean { - return if (settings.isWildcardsEnabled) { - wifiNames.isMatchingToWildcardList(wifiName) - } else { - wifiNames.contains(wifiName) - } - } + private fun hasTrustedWifiName( + wifiName: String, + wifiNames: List = settings.trustedNetworkSSIDs, + ): Boolean { + return if (settings.isWildcardsEnabled) { + wifiNames.isMatchingToWildcardList(wifiName) + } else { + wifiNames.contains(wifiName) + } + } - private fun getTunnelWithMatchingTunnelNetwork(): TunnelConf? { - return networkState.wifiName?.let { wifiName -> - tunnels.firstOrNull { - hasTrustedWifiName(wifiName, it.tunnelNetworks) - } - } - } + private fun getTunnelWithMatchingTunnelNetwork(): TunnelConf? { + return networkState.wifiName?.let { wifiName -> + tunnels.firstOrNull { hasTrustedWifiName(wifiName, it.tunnelNetworks) } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/ConnectivityState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/ConnectivityState.kt index 9788b2a8..ecad7a87 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/ConnectivityState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/ConnectivityState.kt @@ -1,9 +1,9 @@ package com.zaneschepke.wireguardautotunnel.domain.state data class ConnectivityState( - val wifiAvailable: Boolean, - val ethernetAvailable: Boolean, - val cellularAvailable: Boolean, + val wifiAvailable: Boolean, + val ethernetAvailable: Boolean, + val cellularAvailable: Boolean, ) { - val allOffline = !wifiAvailable && !ethernetAvailable && !cellularAvailable + val allOffline = !wifiAvailable && !ethernetAvailable && !cellularAvailable } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/NetworkState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/NetworkState.kt index 040dbd68..917d1d31 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/NetworkState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/NetworkState.kt @@ -1,12 +1,12 @@ package com.zaneschepke.wireguardautotunnel.domain.state data class NetworkState( - val isWifiConnected: Boolean = false, - val isMobileDataConnected: Boolean = false, - val isEthernetConnected: Boolean = false, - val wifiName: String? = null, + val isWifiConnected: Boolean = false, + val isMobileDataConnected: Boolean = false, + val isEthernetConnected: Boolean = false, + val wifiName: String? = null, ) { - fun hasNoCapabilities(): Boolean { - return !isWifiConnected && !isMobileDataConnected && !isEthernetConnected - } + fun hasNoCapabilities(): Boolean { + return !isWifiConnected && !isMobileDataConnected && !isEthernetConnected + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/TunnelState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/TunnelState.kt index 580e6332..164e4d7b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/TunnelState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/TunnelState.kt @@ -4,7 +4,7 @@ import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus data class TunnelState( - val status: TunnelStatus = TunnelStatus.Down, - val backendState: BackendState = BackendState.INACTIVE, - val statistics: TunnelStatistics? = null, + val status: TunnelStatus = TunnelStatus.Down, + val backendState: BackendState = BackendState.INACTIVE, + val statistics: TunnelStatistics? = null, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/TunnelStatistics.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/TunnelStatistics.kt index da5463d1..0356c01e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/TunnelStatistics.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/TunnelStatistics.kt @@ -3,16 +3,20 @@ package com.zaneschepke.wireguardautotunnel.domain.state import org.amnezia.awg.crypto.Key abstract class TunnelStatistics { - @JvmRecord - data class PeerStats(val rxBytes: Long, val txBytes: Long, val latestHandshakeEpochMillis: Long) + @JvmRecord + data class PeerStats( + val rxBytes: Long, + val txBytes: Long, + val latestHandshakeEpochMillis: Long, + ) - abstract fun peerStats(peer: Key): PeerStats? + abstract fun peerStats(peer: Key): PeerStats? - abstract fun isTunnelStale(): Boolean + abstract fun isTunnelStale(): Boolean - abstract fun getPeers(): Array + abstract fun getPeers(): Array - abstract fun rx(): Long + abstract fun rx(): Long - abstract fun tx(): Long + abstract fun tx(): Long } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/WireGuardStatistics.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/WireGuardStatistics.kt index b5c455ee..27932740 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/WireGuardStatistics.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/WireGuardStatistics.kt @@ -4,33 +4,31 @@ import com.wireguard.android.backend.Statistics import org.amnezia.awg.crypto.Key class WireGuardStatistics(private val statistics: Statistics) : TunnelStatistics() { - override fun peerStats(peer: Key): PeerStats? { - val key = com.wireguard.crypto.Key.fromBase64(peer.toBase64()) - val peerStats = statistics.peer(key) - return peerStats?.let { - PeerStats( - txBytes = peerStats.txBytes, - rxBytes = peerStats.rxBytes, - latestHandshakeEpochMillis = peerStats.latestHandshakeEpochMillis, - ) - } - } + override fun peerStats(peer: Key): PeerStats? { + val key = com.wireguard.crypto.Key.fromBase64(peer.toBase64()) + val peerStats = statistics.peer(key) + return peerStats?.let { + PeerStats( + txBytes = peerStats.txBytes, + rxBytes = peerStats.rxBytes, + latestHandshakeEpochMillis = peerStats.latestHandshakeEpochMillis, + ) + } + } - override fun isTunnelStale(): Boolean { - return statistics.isStale - } + override fun isTunnelStale(): Boolean { + return statistics.isStale + } - override fun getPeers(): Array { - return statistics.peers().map { - Key.fromBase64(it.toBase64()) - }.toTypedArray() - } + override fun getPeers(): Array { + return statistics.peers().map { Key.fromBase64(it.toBase64()) }.toTypedArray() + } - override fun rx(): Long { - return statistics.totalRx() - } + override fun rx(): Long { + return statistics.totalRx() + } - override fun tx(): Long { - return statistics.totalTx() - } + override fun tx(): Long { + return statistics.totalTx() + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Route.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Route.kt index ea3a1987..650f237c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Route.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Route.kt @@ -3,69 +3,44 @@ package com.zaneschepke.wireguardautotunnel.ui import kotlinx.serialization.Serializable sealed class Route { - @Serializable - data object Support : Route() + @Serializable data object Support : Route() - @Serializable - data object Settings : Route() + @Serializable data object Settings : Route() - @Serializable - data object SettingsAdvanced : Route() + @Serializable data object SettingsAdvanced : Route() - @Serializable - data object AutoTunnel : Route() + @Serializable data object AutoTunnel : Route() - @Serializable - data object AutoTunnelAdvanced : Route() + @Serializable data object AutoTunnelAdvanced : Route() - @Serializable - data object LocationDisclosure : Route() + @Serializable data object LocationDisclosure : Route() - @Serializable - data object Appearance : Route() + @Serializable data object Appearance : Route() - @Serializable - data object Display : Route() + @Serializable data object Display : Route() - @Serializable - data object KillSwitch : Route() + @Serializable data object KillSwitch : Route() - @Serializable - data object Language : Route() + @Serializable data object Language : Route() - @Serializable - data object Main : Route() + @Serializable data object Main : Route() - @Serializable - data class TunnelOptions( - val id: Int, - ) : Route() + @Serializable data class TunnelOptions(val id: Int) : Route() - @Serializable - data object Lock : Route() + @Serializable data object Lock : Route() - @Serializable - data object Scanner : Route() + @Serializable data object Scanner : Route() - @Serializable - data class Config( - val id: Int, - ) : Route() + @Serializable data class Config(val id: Int) : Route() - @Serializable - data class SplitTunnel( - val id: Int, - ) : Route() { - companion object { - const val KEY_ID = "id" - } - } + @Serializable + data class SplitTunnel(val id: Int) : Route() { + companion object { + const val KEY_ID = "id" + } + } - @Serializable - data class TunnelAutoTunnel( - val id: Int, - ) : Route() + @Serializable data class TunnelAutoTunnel(val id: Int) : Route() - @Serializable - data object Logs : Route() + @Serializable data object Logs : Route() } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/DropdownSelector.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/DropdownSelector.kt index 4eee782f..2e607930 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/DropdownSelector.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/DropdownSelector.kt @@ -20,40 +20,37 @@ import com.zaneschepke.wireguardautotunnel.R @Composable fun DropdownSelector( - currentValue: T, - options: List, - onValueSelected: (T) -> Unit, - modifier: Modifier = Modifier, - label: @Composable (() -> Unit)? = null, - isExpanded: Boolean = false, - onDismiss: () -> Unit = {}, + currentValue: T, + options: List, + onValueSelected: (T) -> Unit, + modifier: Modifier = Modifier, + label: @Composable (() -> Unit)? = null, + isExpanded: Boolean = false, + onDismiss: () -> Unit = {}, ) { - Row( - horizontalArrangement = Arrangement.spacedBy(5.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - if (label != null) label() - Text( - text = currentValue.toString(), - style = MaterialTheme.typography.bodyMedium, - ) - Icon(Icons.Default.ArrowDropDown, contentDescription = stringResource(R.string.dropdown)) - } - DropdownMenu( - modifier = modifier.height(250.dp), - scrollState = rememberScrollState(), - containerColor = MaterialTheme.colorScheme.surface, - expanded = isExpanded, - onDismissRequest = onDismiss, - ) { - options.forEach { option -> - DropdownMenuItem( - text = { Text(text = option.toString()) }, - onClick = { - onValueSelected(option) - onDismiss() // Close dropdown after selection - }, - ) - } - } + Row( + horizontalArrangement = Arrangement.spacedBy(5.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (label != null) label() + Text(text = currentValue.toString(), style = MaterialTheme.typography.bodyMedium) + Icon(Icons.Default.ArrowDropDown, contentDescription = stringResource(R.string.dropdown)) + } + DropdownMenu( + modifier = modifier.height(250.dp), + scrollState = rememberScrollState(), + containerColor = MaterialTheme.colorScheme.surface, + expanded = isExpanded, + onDismissRequest = onDismiss, + ) { + options.forEach { option -> + DropdownMenuItem( + text = { Text(text = option.toString()) }, + onClick = { + onValueSelected(option) + onDismiss() // Close dropdown after selection + }, + ) + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ExpandingRowListItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ExpandingRowListItem.kt index 52594827..18ea739c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ExpandingRowListItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ExpandingRowListItem.kt @@ -22,50 +22,43 @@ import androidx.compose.ui.unit.dp @OptIn(ExperimentalFoundationApi::class) @Composable fun ExpandingRowListItem( - leading: @Composable () -> Unit, - text: String, - onHold: () -> Unit = {}, - onClick: () -> Unit, - trailing: @Composable () -> Unit, - isExpanded: Boolean, - expanded: @Composable () -> Unit = {}, + leading: @Composable () -> Unit, + text: String, + onHold: () -> Unit = {}, + onClick: () -> Unit, + trailing: @Composable () -> Unit, + isExpanded: Boolean, + expanded: @Composable () -> Unit = {}, ) { - Box( - modifier = - Modifier - .animateContentSize() - .clip(RoundedCornerShape(30.dp)) - .combinedClickable( - onClick = { onClick() }, - onLongClick = { onHold() }, - ), - ) { - Column { - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 15.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(15.dp), - modifier = Modifier.fillMaxWidth(13 / 20f), - ) { - leading() - Text( - text, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.onBackground, - ) - } - trailing() - } - if (isExpanded) expanded() - } - } + Box( + modifier = + Modifier.animateContentSize() + .clip(RoundedCornerShape(30.dp)) + .combinedClickable(onClick = { onClick() }, onLongClick = { onHold() }) + ) { + Column { + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 15.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(15.dp), + modifier = Modifier.fillMaxWidth(13 / 20f), + ) { + leading() + Text( + text, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onBackground, + ) + } + trailing() + } + if (isExpanded) expanded() + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/animation/ShimmerEffect.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/animation/ShimmerEffect.kt index 2655a071..843d10f3 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/animation/ShimmerEffect.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/animation/ShimmerEffect.kt @@ -14,24 +14,25 @@ import androidx.compose.ui.graphics.Color @Composable fun ShimmerEffect(modifier: Modifier = Modifier): Brush { - val shimmerColors = listOf( - Color.LightGray.copy(alpha = 0.9f), - Color.LightGray.copy(alpha = 0.3f), - Color.LightGray.copy(alpha = 0.9f), - ) + val shimmerColors = + listOf( + Color.LightGray.copy(alpha = 0.9f), + Color.LightGray.copy(alpha = 0.3f), + Color.LightGray.copy(alpha = 0.9f), + ) - val transition = rememberInfiniteTransition() - val translateAnim by transition.animateFloat( - initialValue = 0f, - targetValue = 1000f, - animationSpec = infiniteRepeatable( - animation = tween(durationMillis = 1200, easing = LinearEasing), - ), - ) + val transition = rememberInfiniteTransition() + val translateAnim by + transition.animateFloat( + initialValue = 0f, + targetValue = 1000f, + animationSpec = + infiniteRepeatable(animation = tween(durationMillis = 1200, easing = LinearEasing)), + ) - return Brush.linearGradient( - colors = shimmerColors, - start = Offset(0f, 0f), - end = Offset(translateAnim, translateAnim), - ) + return Brush.linearGradient( + colors = shimmerColors, + start = Offset(0f, 0f), + end = Offset(translateAnim, translateAnim), + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/ClickableIconButton.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/ClickableIconButton.kt index cccca123..854ec7a4 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/ClickableIconButton.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/ClickableIconButton.kt @@ -13,25 +13,30 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector @Composable -fun ClickableIconButton(onClick: () -> Unit, onIconClick: () -> Unit, text: String, icon: ImageVector, enabled: Boolean = true) { - TextButton( - onClick = onClick, - enabled = enabled, - ) { - Text(text, Modifier.weight(1f, false), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.primary) - Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing)) - Icon( - imageVector = icon, - contentDescription = icon.name, - modifier = - Modifier - .size(ButtonDefaults.IconSize) - .weight(1f, false) - .clickable { - if (enabled) { - onIconClick() - } - }, - ) - } +fun ClickableIconButton( + onClick: () -> Unit, + onIconClick: () -> Unit, + text: String, + icon: ImageVector, + enabled: Boolean = true, +) { + TextButton(onClick = onClick, enabled = enabled) { + Text( + text, + Modifier.weight(1f, false), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.primary, + ) + Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing)) + Icon( + imageVector = icon, + contentDescription = icon.name, + modifier = + Modifier.size(ButtonDefaults.IconSize).weight(1f, false).clickable { + if (enabled) { + onIconClick() + } + }, + ) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/ForwardButton.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/ForwardButton.kt index 0dbb8771..596d6f3d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/ForwardButton.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/ForwardButton.kt @@ -12,11 +12,8 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize @Composable fun ForwardButton(modifier: Modifier = Modifier.focusable(), onClick: () -> Unit) { - IconButton( - modifier = modifier, - onClick = onClick, - ) { - val icon = Icons.AutoMirrored.Outlined.ArrowForward - Icon(icon, icon.name, Modifier.size(iconSize)) - } + IconButton(modifier = modifier, onClick = onClick) { + val icon = Icons.AutoMirrored.Outlined.ArrowForward + Icon(icon, icon.name, Modifier.size(iconSize)) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/IconSurfaceButton.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/IconSurfaceButton.kt index 042a3d89..f0968377 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/IconSurfaceButton.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/IconSurfaceButton.kt @@ -25,73 +25,67 @@ import androidx.compose.ui.unit.dp import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize @androidx.compose.runtime.Composable -fun IconSurfaceButton(title: String, onClick: () -> Unit, selected: Boolean, leadingIcon: ImageVector? = null, description: String? = null) { - val border: BorderStroke? = - if (selected) { - BorderStroke( - 1.dp, - MaterialTheme.colorScheme.primary, - ) - } else { - null - } - Card( - modifier = - Modifier - .fillMaxWidth() - .height(IntrinsicSize.Min), - shape = RoundedCornerShape(8.dp), - border = border, - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), - ) { - Box( - modifier = Modifier.clickable { onClick() } - .fillMaxWidth(), - ) { - Column( - modifier = - Modifier - .padding(horizontal = 8.dp, vertical = 10.dp) - .padding(end = 16.dp).padding(start = 8.dp) - .fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.Start, - ) { - Row( - verticalAlignment = Alignment.Companion.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(16.dp), - ) { - Row( - horizontalArrangement = Arrangement.spacedBy( - 16.dp, - ), - verticalAlignment = Alignment.Companion.CenterVertically, - modifier = Modifier.padding(vertical = if (description == null) 10.dp else 0.dp), - ) { - leadingIcon?.let { - Icon( - leadingIcon, - leadingIcon.name, - Modifier.size(iconSize), - if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface, - ) - } - Column { - Text( - title, - style = MaterialTheme.typography.titleMedium, - ) - description?.let { - Text( - description, - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodyMedium, - ) - } - } - } - } - } - } - } +fun IconSurfaceButton( + title: String, + onClick: () -> Unit, + selected: Boolean, + leadingIcon: ImageVector? = null, + description: String? = null, +) { + val border: BorderStroke? = + if (selected) { + BorderStroke(1.dp, MaterialTheme.colorScheme.primary) + } else { + null + } + Card( + modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min), + shape = RoundedCornerShape(8.dp), + border = border, + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + ) { + Box(modifier = Modifier.clickable { onClick() }.fillMaxWidth()) { + Column( + modifier = + Modifier.padding(horizontal = 8.dp, vertical = 10.dp) + .padding(end = 16.dp) + .padding(start = 8.dp) + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start, + ) { + Row( + verticalAlignment = Alignment.Companion.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.Companion.CenterVertically, + modifier = + Modifier.padding(vertical = if (description == null) 10.dp else 0.dp), + ) { + leadingIcon?.let { + Icon( + leadingIcon, + leadingIcon.name, + Modifier.size(iconSize), + if (selected) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurface, + ) + } + Column { + Text(title, style = MaterialTheme.typography.titleMedium) + description?.let { + Text( + description, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + ) + } + } + } + } + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/ScaledSwitch.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/ScaledSwitch.kt index 16475e10..1d082cc1 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/ScaledSwitch.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/ScaledSwitch.kt @@ -9,19 +9,26 @@ import androidx.compose.ui.draw.scale import androidx.compose.ui.unit.dp @Composable -fun ScaledSwitch(checked: Boolean, onClick: (checked: Boolean) -> Unit, enabled: Boolean = true, modifier: Modifier = Modifier) { - Switch( - checked, - { onClick(it) }, - modifier.scale((52.dp / 52.dp)), - enabled = enabled, - colors = SwitchDefaults.colors().copy( - checkedThumbColor = MaterialTheme.colorScheme.background, - checkedIconColor = MaterialTheme.colorScheme.background, - uncheckedTrackColor = MaterialTheme.colorScheme.surface, - uncheckedBorderColor = MaterialTheme.colorScheme.outline, - uncheckedThumbColor = MaterialTheme.colorScheme.outline, - uncheckedIconColor = MaterialTheme.colorScheme.outline, - ), - ) +fun ScaledSwitch( + checked: Boolean, + onClick: (checked: Boolean) -> Unit, + enabled: Boolean = true, + modifier: Modifier = Modifier, +) { + Switch( + checked, + { onClick(it) }, + modifier.scale((52.dp / 52.dp)), + enabled = enabled, + colors = + SwitchDefaults.colors() + .copy( + checkedThumbColor = MaterialTheme.colorScheme.background, + checkedIconColor = MaterialTheme.colorScheme.background, + uncheckedTrackColor = MaterialTheme.colorScheme.surface, + uncheckedBorderColor = MaterialTheme.colorScheme.outline, + uncheckedThumbColor = MaterialTheme.colorScheme.outline, + uncheckedIconColor = MaterialTheme.colorScheme.outline, + ), + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/SelectionItemButton.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/SelectionItemButton.kt index bd61c0aa..3a47118a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/SelectionItemButton.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/SelectionItemButton.kt @@ -25,49 +25,40 @@ import androidx.compose.ui.unit.dp @Composable fun SelectionItemButton( - leading: (@Composable () -> Unit)? = null, - buttonText: String, - trailing: (@Composable () -> Unit)? = null, - onClick: () -> Unit, - ripple: Boolean = true, - modifier: Modifier = Modifier, + leading: (@Composable () -> Unit)? = null, + buttonText: String, + trailing: (@Composable () -> Unit)? = null, + onClick: () -> Unit, + ripple: Boolean = true, + modifier: Modifier = Modifier, ) { - Card( - modifier = - modifier - .clip(RoundedCornerShape(8.dp)) - .clickable( - indication = if (ripple) ripple() else null, - interactionSource = remember { MutableInteractionSource() }, - onClick = { onClick() }, - ) - .height(56.dp), - colors = - CardDefaults.cardColors( - containerColor = Color.Transparent, - ), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Start, - modifier = Modifier - .fillMaxSize() - .padding(end = 10.dp), - ) { - leading?.let { - it() - } - Text( - buttonText, - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.fillMaxWidth(3 / 4f), - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - trailing?.let { - it() - } - } - } + Card( + modifier = + modifier + .clip(RoundedCornerShape(8.dp)) + .clickable( + indication = if (ripple) ripple() else null, + interactionSource = remember { MutableInteractionSource() }, + onClick = { onClick() }, + ) + .height(56.dp), + colors = CardDefaults.cardColors(containerColor = Color.Transparent), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + modifier = Modifier.fillMaxSize().padding(end = 10.dp), + ) { + leading?.let { it() } + Text( + buttonText, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.fillMaxWidth(3 / 4f), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + trailing?.let { it() } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SelectionItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SelectionItem.kt index f92eec43..9f79e96f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SelectionItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SelectionItem.kt @@ -4,10 +4,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.vector.ImageVector data class SelectionItem( - val leadingIcon: ImageVector? = null, - val trailing: (@Composable () -> Unit)? = null, - val title: (@Composable () -> Unit), - val description: (@Composable () -> Unit)? = null, - val onClick: (() -> Unit)? = null, - val height: Int = 64, + val leadingIcon: ImageVector? = null, + val trailing: (@Composable () -> Unit)? = null, + val title: (@Composable () -> Unit), + val description: (@Composable () -> Unit)? = null, + val onClick: (() -> Unit)? = null, + val height: Int = 64, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SelectionItemLabel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SelectionItemLabel.kt index bc6071c3..6eccb4bd 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SelectionItemLabel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SelectionItemLabel.kt @@ -7,14 +7,17 @@ import androidx.compose.ui.res.stringResource @Composable fun SelectionItemLabel( - textResId: Int, - style: androidx.compose.ui.text.TextStyle = MaterialTheme.typography.bodyMedium, - isDescription: Boolean = false, + textResId: Int, + style: androidx.compose.ui.text.TextStyle = MaterialTheme.typography.bodyMedium, + isDescription: Boolean = false, ) { - Text( - text = stringResource(textResId), - style = style.copy( - color = if (isDescription) MaterialTheme.colorScheme.outline else MaterialTheme.colorScheme.onSurface, - ), - ) + Text( + text = stringResource(textResId), + style = + style.copy( + color = + if (isDescription) MaterialTheme.colorScheme.outline + else MaterialTheme.colorScheme.onSurface + ), + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SurfaceSelectionGroupButton.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SurfaceSelectionGroupButton.kt index ec255c63..451fb1a0 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SurfaceSelectionGroupButton.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SurfaceSelectionGroupButton.kt @@ -22,63 +22,61 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize @Composable fun SurfaceSelectionGroupButton(items: List) { - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(8.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), - ) { - items.mapIndexed { index, item -> - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .then(item.onClick?.let { Modifier.clickable { it() } } ?: Modifier) - .fillMaxWidth(), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .weight(4f, false) - .fillMaxWidth(), - ) { - item.leadingIcon?.let { icon -> - Icon( - icon, - icon.name, - modifier = Modifier.size(iconSize), - tint = MaterialTheme.colorScheme.onSurface, - ) - } - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically), - modifier = Modifier - .fillMaxWidth() - .padding(start = if (item.leadingIcon != null) 16.dp else 0.dp) - .padding(vertical = if (item.description == null) 16.dp else 6.dp), - ) { - item.title() - item.description?.let { - it() - } - } - } - item.trailing?.let { - Box( - contentAlignment = Alignment.CenterEnd, - modifier = Modifier - .padding(start = 16.dp) - .weight(1f), - ) { - it() - } - } - } - } - if (index + 1 != items.size) HorizontalDivider(color = MaterialTheme.colorScheme.outline.copy(0.30f)) - } - } + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(8.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + ) { + items.mapIndexed { index, item -> + Box( + contentAlignment = Alignment.Center, + modifier = + Modifier.then(item.onClick?.let { Modifier.clickable { it() } } ?: Modifier) + .fillMaxWidth(), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.weight(4f, false).fillMaxWidth(), + ) { + item.leadingIcon?.let { icon -> + Icon( + icon, + icon.name, + modifier = Modifier.size(iconSize), + tint = MaterialTheme.colorScheme.onSurface, + ) + } + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = + Arrangement.spacedBy(2.dp, Alignment.CenterVertically), + modifier = + Modifier.fillMaxWidth() + .padding(start = if (item.leadingIcon != null) 16.dp else 0.dp) + .padding( + vertical = if (item.description == null) 16.dp else 6.dp + ), + ) { + item.title() + item.description?.let { it() } + } + } + item.trailing?.let { + Box( + contentAlignment = Alignment.CenterEnd, + modifier = Modifier.padding(start = 16.dp).weight(1f), + ) { + it() + } + } + } + } + if (index + 1 != items.size) + HorizontalDivider(color = MaterialTheme.colorScheme.outline.copy(0.30f)) + } + } } 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 4480f0ee..7e95b910 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 @@ -13,33 +13,43 @@ import androidx.compose.ui.text.input.KeyboardCapitalization @Composable fun ConfigurationTextBox( - value: String, - hint: String, - onValueChange: (String) -> Unit, - keyboardActions: KeyboardActions = KeyboardActions(), - label: String, - modifier: Modifier, - isError: Boolean = false, - keyboardOptions: KeyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.None, - imeAction = ImeAction.Done, - ), - trailing: (@Composable () -> Unit)? = null, - interactionSource: MutableInteractionSource? = null, + value: String, + hint: String, + onValueChange: (String) -> Unit, + keyboardActions: KeyboardActions = KeyboardActions(), + label: String, + modifier: Modifier, + isError: Boolean = false, + keyboardOptions: KeyboardOptions = + KeyboardOptions(capitalization = KeyboardCapitalization.None, imeAction = ImeAction.Done), + trailing: (@Composable () -> Unit)? = null, + interactionSource: MutableInteractionSource? = null, ) { - OutlinedTextField( - isError = isError, - textStyle = MaterialTheme.typography.labelLarge, - modifier = modifier, - value = value, - singleLine = true, - interactionSource = interactionSource, - onValueChange = { onValueChange(it) }, - label = { Text(label, color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.bodyMedium) }, - maxLines = 1, - placeholder = { Text(hint, color = MaterialTheme.colorScheme.outline, style = MaterialTheme.typography.bodyMedium) }, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - trailingIcon = trailing, - ) + OutlinedTextField( + isError = isError, + textStyle = MaterialTheme.typography.labelLarge, + modifier = modifier, + value = value, + singleLine = true, + interactionSource = interactionSource, + onValueChange = { onValueChange(it) }, + label = { + Text( + label, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodyMedium, + ) + }, + maxLines = 1, + placeholder = { + Text( + hint, + color = MaterialTheme.colorScheme.outline, + style = MaterialTheme.typography.bodyMedium, + ) + }, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + trailingIcon = trailing, + ) } 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 06df35d5..8de19317 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 @@ -13,36 +13,29 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch @Composable fun ConfigurationToggle( - label: String, - enabled: Boolean = true, - checked: Boolean, - onCheckChanged: (checked: Boolean) -> Unit, - modifier: Modifier = Modifier, + label: String, + enabled: Boolean = true, + checked: Boolean, + onCheckChanged: (checked: Boolean) -> Unit, + modifier: Modifier = Modifier, ) { - Row( - modifier = - Modifier - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - label, - textAlign = TextAlign.Start, - style = MaterialTheme.typography.labelLarge, - modifier = - Modifier - .weight( - weight = 1.0f, - fill = false, - ), - softWrap = true, - ) - ScaledSwitch( - modifier = modifier, - enabled = enabled, - checked = checked, - onClick = { onCheckChanged(it) }, - ) - } + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + label, + textAlign = TextAlign.Start, + style = MaterialTheme.typography.labelLarge, + modifier = Modifier.weight(weight = 1.0f, fill = false), + softWrap = true, + ) + ScaledSwitch( + modifier = modifier, + enabled = enabled, + checked = checked, + onClick = { onCheckChanged(it) }, + ) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/SubmitConfigurationTextBox.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/SubmitConfigurationTextBox.kt index c28402e0..725975ac 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/SubmitConfigurationTextBox.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/SubmitConfigurationTextBox.kt @@ -27,61 +27,70 @@ import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField @Composable fun SubmitConfigurationTextBox( - value: String?, - label: String, - hint: String, - isErrorValue: (value: String?) -> Boolean, - onSubmit: (value: String) -> Unit, - keyboardOptions: KeyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.None, - imeAction = ImeAction.Done, - ), + value: String?, + label: String, + hint: String, + isErrorValue: (value: String?) -> Boolean, + onSubmit: (value: String) -> Unit, + keyboardOptions: KeyboardOptions = + KeyboardOptions(capitalization = KeyboardCapitalization.None, imeAction = ImeAction.Done), ) { - val focusManager = LocalFocusManager.current - val interactionSource = remember { MutableInteractionSource() } - val isFocused by interactionSource.collectIsFocusedAsState() - val keyboardController = LocalSoftwareKeyboardController.current + val focusManager = LocalFocusManager.current + val interactionSource = remember { MutableInteractionSource() } + val isFocused by interactionSource.collectIsFocusedAsState() + val keyboardController = LocalSoftwareKeyboardController.current - var stateValue by remember { mutableStateOf(value ?: "") } + var stateValue by remember { mutableStateOf(value ?: "") } - CustomTextField( - isError = isErrorValue(stateValue), - textStyle = MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.onSurface), - value = stateValue, - onValueChange = { stateValue = it }, - interactionSource = interactionSource, - label = { Text(label, color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.labelMedium) }, - containerColor = MaterialTheme.colorScheme.surface, - placeholder = { Text(hint, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline) }, - modifier = - Modifier - .padding( - top = 5.dp, - bottom = 10.dp, - ).fillMaxWidth().padding(end = 16.dp), - singleLine = true, - keyboardOptions = keyboardOptions, - keyboardActions = KeyboardActions( - onDone = { - onSubmit(stateValue) - keyboardController?.hide() - }, - ), - trailing = { - if (!isErrorValue(stateValue) && isFocused) { - IconButton(onClick = { - onSubmit(stateValue) - keyboardController?.hide() - focusManager.clearFocus() - }) { - val icon = Icons.Outlined.Save - Icon( - imageVector = icon, - contentDescription = icon.name, - tint = MaterialTheme.colorScheme.primary, - ) - } - } - }, - ) + CustomTextField( + isError = isErrorValue(stateValue), + textStyle = + MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.onSurface), + value = stateValue, + onValueChange = { stateValue = it }, + interactionSource = interactionSource, + label = { + Text( + label, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelMedium, + ) + }, + containerColor = MaterialTheme.colorScheme.surface, + placeholder = { + Text( + hint, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.outline, + ) + }, + modifier = Modifier.padding(top = 5.dp, bottom = 10.dp).fillMaxWidth().padding(end = 16.dp), + singleLine = true, + keyboardOptions = keyboardOptions, + keyboardActions = + KeyboardActions( + onDone = { + onSubmit(stateValue) + keyboardController?.hide() + } + ), + trailing = { + if (!isErrorValue(stateValue) && isFocused) { + IconButton( + onClick = { + onSubmit(stateValue) + keyboardController?.hide() + focusManager.clearFocus() + } + ) { + val icon = Icons.Outlined.Save + Icon( + imageVector = icon, + contentDescription = icon.name, + tint = MaterialTheme.colorScheme.primary, + ) + } + } + }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dialog/InfoDialog.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dialog/InfoDialog.kt index 5f31277a..3a52c10c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dialog/InfoDialog.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dialog/InfoDialog.kt @@ -13,38 +13,27 @@ import com.zaneschepke.wireguardautotunnel.R @Composable fun InfoDialog( - onAttest: () -> Unit, - onDismiss: () -> Unit, - title: @Composable () -> Unit, - body: @Composable () -> Unit, - confirmText: @Composable () -> Unit, + onAttest: () -> Unit, + onDismiss: () -> Unit, + title: @Composable () -> Unit, + body: @Composable () -> Unit, + confirmText: @Composable () -> Unit, ) { - MaterialTheme( - colorScheme = MaterialTheme.colorScheme.copy(), - ) { - Surface( - color = MaterialTheme.colorScheme.surface, - tonalElevation = 0.dp, - ) { - AlertDialog( - onDismissRequest = { onDismiss() }, - confirmButton = { - TextButton(onClick = { onAttest() }) { - confirmText() - } - }, - dismissButton = { - TextButton(onClick = { onDismiss() }) { - Text(text = stringResource(R.string.cancel)) - } - }, - containerColor = MaterialTheme.colorScheme.surface, - title = { title() }, - text = { body() }, - properties = DialogProperties( - usePlatformDefaultWidth = true, - ), - ) - } - } + MaterialTheme(colorScheme = MaterialTheme.colorScheme.copy()) { + Surface(color = MaterialTheme.colorScheme.surface, tonalElevation = 0.dp) { + AlertDialog( + onDismissRequest = { onDismiss() }, + confirmButton = { TextButton(onClick = { onAttest() }) { confirmText() } }, + dismissButton = { + TextButton(onClick = { onDismiss() }) { + Text(text = stringResource(R.string.cancel)) + } + }, + containerColor = MaterialTheme.colorScheme.surface, + title = { title() }, + text = { body() }, + properties = DialogProperties(usePlatformDefaultWidth = true), + ) + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dialog/VpnDeniedDialog.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dialog/VpnDeniedDialog.kt index e687c1dc..09bd1647 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dialog/VpnDeniedDialog.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dialog/VpnDeniedDialog.kt @@ -14,35 +14,39 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.launchVpnSettings @Composable fun VpnDeniedDialog(show: Boolean, onDismiss: () -> Unit) { - val context = LocalContext.current - if (show) { - val alwaysOnDescription = buildAnnotatedString { - append(stringResource(R.string.always_on_message)) - append(" ") - pushStringAnnotation(tag = "vpnSettings", annotation = "") - withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) { - append(stringResource(id = R.string.vpn_settings)) - } - pop() - append(" ") - append(stringResource(R.string.always_on_message2)) - append(".") - } - InfoDialog( - onDismiss = { onDismiss() }, - onAttest = { onDismiss() }, - title = { Text(text = stringResource(R.string.vpn_denied_dialog_title)) }, - body = { - ClickableText( - text = alwaysOnDescription, - style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.outline), - ) { - alwaysOnDescription.getStringAnnotations(tag = "vpnSettings", it, it).firstOrNull()?.let { - context.launchVpnSettings() - } - } - }, - confirmText = { Text(text = stringResource(R.string.okay)) }, - ) - } + val context = LocalContext.current + if (show) { + val alwaysOnDescription = buildAnnotatedString { + append(stringResource(R.string.always_on_message)) + append(" ") + pushStringAnnotation(tag = "vpnSettings", annotation = "") + withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) { + append(stringResource(id = R.string.vpn_settings)) + } + pop() + append(" ") + append(stringResource(R.string.always_on_message2)) + append(".") + } + InfoDialog( + onDismiss = { onDismiss() }, + onAttest = { onDismiss() }, + title = { Text(text = stringResource(R.string.vpn_denied_dialog_title)) }, + body = { + ClickableText( + text = alwaysOnDescription, + style = + MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.outline + ), + ) { + alwaysOnDescription + .getStringAnnotations(tag = "vpnSettings", it, it) + .firstOrNull() + ?.let { context.launchVpnSettings() } + } + }, + confirmText = { Text(text = stringResource(R.string.okay)) }, + ) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/functions/Functions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/functions/Functions.kt index 93326552..fc662828 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/functions/Functions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/functions/Functions.kt @@ -13,48 +13,53 @@ import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv @Composable -fun rememberFileImportLauncherForResult(onNoFileExplorer: () -> Unit, onData: (data: Uri) -> Unit): ManagedActivityResultLauncher { - return rememberLauncherForActivityResult( - object : ActivityResultContracts.GetContent() { - override fun createIntent(context: Context, input: String): Intent { - val intent = super.createIntent(context, input).apply { - type = if (context.isRunningOnTv()) { - Constants.ALLOWED_TV_FILE_TYPES - } else { - Constants.ALL_FILE_TYPES - } - } +fun rememberFileImportLauncherForResult( + onNoFileExplorer: () -> Unit, + onData: (data: Uri) -> Unit, +): ManagedActivityResultLauncher { + return rememberLauncherForActivityResult( + object : ActivityResultContracts.GetContent() { + override fun createIntent(context: Context, input: String): Intent { + val intent = + super.createIntent(context, input).apply { + type = + if (context.isRunningOnTv()) { + Constants.ALLOWED_TV_FILE_TYPES + } else { + Constants.ALL_FILE_TYPES + } + } - /* 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) - } - ) { - onNoFileExplorer() - } - return intent - } - }, - ) { data -> - if (data == null) return@rememberLauncherForActivityResult - onData(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) + } + ) { + onNoFileExplorer() + } + return intent + } + } + ) { data -> + if (data == null) return@rememberLauncherForActivityResult + onData(data) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/label/GroupLabel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/label/GroupLabel.kt index 957e9b54..01b53ef2 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/label/GroupLabel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/label/GroupLabel.kt @@ -10,15 +10,12 @@ import androidx.compose.ui.text.font.FontWeight @Composable fun GroupLabel(title: String) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Start, - ) { - Text( - title, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onBackground, - ) - } + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start) { + Text( + title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onBackground, + ) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/label/SelectedLabel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/label/SelectedLabel.kt index df64cfdc..ea3a6dad 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/label/SelectedLabel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/label/SelectedLabel.kt @@ -12,12 +12,12 @@ import androidx.compose.ui.Modifier @Composable fun SelectedLabel() { - Row( - modifier = Modifier.fillMaxSize(), - horizontalArrangement = Arrangement.End, - verticalAlignment = Alignment.CenterVertically, - ) { - val icon = Icons.Outlined.CheckBox - Icon(icon, icon.name) - } + Row( + modifier = Modifier.fillMaxSize(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + val icon = Icons.Outlined.CheckBox + Icon(icon, icon.name) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomBarTabs.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomBarTabs.kt index 8c25079e..63aea8c5 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomBarTabs.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomBarTabs.kt @@ -32,80 +32,84 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv @Composable -fun BottomBarTabs(tabs: List, selectedTabIndex: Int, isChildRoute: Boolean, onTabSelected: (BottomNavItem) -> Unit) { - val context = LocalContext.current - val isRunningOnTv = remember { context.isRunningOnTv() } +fun BottomBarTabs( + tabs: List, + selectedTabIndex: Int, + isChildRoute: Boolean, + onTabSelected: (BottomNavItem) -> Unit, +) { + val context = LocalContext.current + val isRunningOnTv = remember { context.isRunningOnTv() } - Row( - modifier = Modifier - .fillMaxWidth() - .height(64.dp) - .padding(horizontal = 8.dp) - .padding(top = 12.dp), - horizontalArrangement = Arrangement.SpaceEvenly, - verticalAlignment = Alignment.CenterVertically, - ) { - tabs.forEachIndexed { index, tab -> - Column( - modifier = Modifier - .weight(1f) - .fillMaxHeight() - .background(Color.Transparent) - .then( - if (isRunningOnTv) { - Modifier.clickable { - if (index == selectedTabIndex && !isChildRoute) return@clickable - tab.onClick.invoke() - onTabSelected(tab) - } - } else { - Modifier - }, - ) - .pointerInput(Unit) { - detectTapGestures { - if (index == selectedTabIndex && !isChildRoute) return@detectTapGestures - tab.onClick.invoke() - onTabSelected(tab) - } - }, - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - val animatedColor by animateColorAsState( - targetValue = MaterialTheme.colorScheme.primary, - animationSpec = spring(stiffness = Spring.StiffnessLow), - label = "animatedColor", - ) - val color = if (selectedTabIndex == index) animatedColor else MaterialTheme.colorScheme.onSurface + Row( + modifier = + Modifier.fillMaxWidth().height(64.dp).padding(horizontal = 8.dp).padding(top = 12.dp), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + ) { + tabs.forEachIndexed { index, tab -> + Column( + modifier = + Modifier.weight(1f) + .fillMaxHeight() + .background(Color.Transparent) + .then( + if (isRunningOnTv) { + Modifier.clickable { + if (index == selectedTabIndex && !isChildRoute) return@clickable + tab.onClick.invoke() + onTabSelected(tab) + } + } else { + Modifier + } + ) + .pointerInput(Unit) { + detectTapGestures { + if (index == selectedTabIndex && !isChildRoute) + return@detectTapGestures + tab.onClick.invoke() + onTabSelected(tab) + } + }, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + val animatedColor by + animateColorAsState( + targetValue = MaterialTheme.colorScheme.primary, + animationSpec = spring(stiffness = Spring.StiffnessLow), + label = "animatedColor", + ) + val color = + if (selectedTabIndex == index) animatedColor + else MaterialTheme.colorScheme.onSurface - if (tab.active) { - BadgedBox( - badge = { - Badge( - modifier = Modifier - .offset(x = 8.dp, y = ((-8).dp)) - .size(6.dp), - containerColor = SilverTree, - ) - }, - ) { - Icon( - imageVector = tab.icon, - contentDescription = tab.name, - tint = color, - modifier = Modifier.size(24.dp), - ) - } - } else { - Icon( - imageVector = tab.icon, - contentDescription = tab.name, - tint = color, - modifier = Modifier.size(24.dp), - ) - } - } - } - } + if (tab.active) { + BadgedBox( + badge = { + Badge( + modifier = Modifier.offset(x = 8.dp, y = ((-8).dp)).size(6.dp), + containerColor = SilverTree, + ) + } + ) { + Icon( + imageVector = tab.icon, + contentDescription = tab.name, + tint = color, + modifier = Modifier.size(24.dp), + ) + } + } else { + Icon( + imageVector = tab.icon, + contentDescription = tab.name, + tint = color, + modifier = Modifier.size(24.dp), + ) + } + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavItem.kt index 33a0ab86..326c3034 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavItem.kt @@ -4,9 +4,9 @@ import androidx.compose.ui.graphics.vector.ImageVector import com.zaneschepke.wireguardautotunnel.ui.Route data class BottomNavItem( - val name: String, - val route: Route, - val icon: ImageVector, - val onClick: () -> Unit, - val active: Boolean = false, + val name: String, + val route: Route, + val icon: ImageVector, + val onClick: () -> Unit, + val active: Boolean = false, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/CustomBottomNavbar.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/CustomBottomNavbar.kt index 07ea30cc..b3b1089a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/CustomBottomNavbar.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/CustomBottomNavbar.kt @@ -35,99 +35,87 @@ import com.zaneschepke.wireguardautotunnel.ui.Route @Composable fun CustomBottomNavbar(tabs: List, navBarState: NavBarState) { - var selectedTabIndex by remember { mutableIntStateOf(0) } - var isChildRoute by remember { mutableStateOf(false) } + var selectedTabIndex by remember { mutableIntStateOf(0) } + var isChildRoute by remember { mutableStateOf(false) } - LaunchedEffect(tabs) { - } - when (navBarState.route) { - Route.Main -> { - selectedTabIndex = 0 - isChildRoute = false - } - Route.AutoTunnel -> { - selectedTabIndex = 1 - isChildRoute = false - } - Route.Settings -> { - selectedTabIndex = 2 - isChildRoute = false - } - Route.Support -> { - selectedTabIndex = 3 - isChildRoute = false - } - else -> isChildRoute = true - } + LaunchedEffect(tabs) {} + when (navBarState.route) { + Route.Main -> { + selectedTabIndex = 0 + isChildRoute = false + } + Route.AutoTunnel -> { + selectedTabIndex = 1 + isChildRoute = false + } + Route.Settings -> { + selectedTabIndex = 2 + isChildRoute = false + } + Route.Support -> { + selectedTabIndex = 3 + isChildRoute = false + } + else -> isChildRoute = true + } - val systemBars = WindowInsets.systemBars - val bottomPadding = with(LocalDensity.current) { - systemBars.getBottom(this).toDp() - } - val navHeight = 64.dp + bottomPadding + val systemBars = WindowInsets.systemBars + val bottomPadding = with(LocalDensity.current) { systemBars.getBottom(this).toDp() } + val navHeight = 64.dp + bottomPadding - Box( - modifier = Modifier - .fillMaxWidth() - .height(navHeight) - .background(Color.Transparent), - ) { - BottomBarTabs( - tabs = tabs, - selectedTabIndex = selectedTabIndex, - isChildRoute = isChildRoute, - onTabSelected = { - selectedTabIndex = tabs.indexOf(it) - }, - ) + Box(modifier = Modifier.fillMaxWidth().height(navHeight).background(Color.Transparent)) { + BottomBarTabs( + tabs = tabs, + selectedTabIndex = selectedTabIndex, + isChildRoute = isChildRoute, + onTabSelected = { selectedTabIndex = tabs.indexOf(it) }, + ) - val animatedSelectedTabIndex by animateFloatAsState( - targetValue = selectedTabIndex.toFloat(), - label = "animatedSelectedTabIndex", - animationSpec = spring( - stiffness = Spring.StiffnessLow, - dampingRatio = Spring.DampingRatioLowBouncy, - ), - ) + val animatedSelectedTabIndex by + animateFloatAsState( + targetValue = selectedTabIndex.toFloat(), + label = "animatedSelectedTabIndex", + animationSpec = + spring( + stiffness = Spring.StiffnessLow, + dampingRatio = Spring.DampingRatioLowBouncy, + ), + ) - val animatedColor by animateColorAsState( - targetValue = MaterialTheme.colorScheme.primary, - label = "animatedColor", - animationSpec = spring( - stiffness = Spring.StiffnessLow, - ), - ) + val animatedColor by + animateColorAsState( + targetValue = MaterialTheme.colorScheme.primary, + label = "animatedColor", + animationSpec = spring(stiffness = Spring.StiffnessLow), + ) - Canvas( - modifier = Modifier - .fillMaxWidth() - .height(navHeight), - ) { - val path = Path().apply { - addRoundRect(RoundRect(size.toRect(), CornerRadius(size.height))) - } - val length = PathMeasure().apply { setPath(path, false) }.length + Canvas(modifier = Modifier.fillMaxWidth().height(navHeight)) { + val path = + Path().apply { addRoundRect(RoundRect(size.toRect(), CornerRadius(size.height))) } + val length = PathMeasure().apply { setPath(path, false) }.length - val tabWidth = size.width / tabs.size - drawPath( - path, - brush = Brush.horizontalGradient( - colors = listOf( - animatedColor.copy(alpha = 0f), - animatedColor.copy(alpha = 1f), - animatedColor.copy(alpha = 1f), - animatedColor.copy(alpha = 0f), - ), - startX = tabWidth * animatedSelectedTabIndex, - endX = tabWidth * (animatedSelectedTabIndex + 1), - ), - style = Stroke( - width = 4f, - pathEffect = PathEffect.dashPathEffect( - intervals = floatArrayOf(length / 2, length), - ), - ), - ) - } - } + val tabWidth = size.width / tabs.size + drawPath( + path, + brush = + Brush.horizontalGradient( + colors = + listOf( + animatedColor.copy(alpha = 0f), + animatedColor.copy(alpha = 1f), + animatedColor.copy(alpha = 1f), + animatedColor.copy(alpha = 0f), + ), + startX = tabWidth * animatedSelectedTabIndex, + endX = tabWidth * (animatedSelectedTabIndex + 1), + ), + style = + Stroke( + width = 4f, + pathEffect = + PathEffect.dashPathEffect(intervals = floatArrayOf(length / 2, length)), + ), + ) + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/DynamicTopAppBar.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/DynamicTopAppBar.kt index bd63bfb8..128f6bc7 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/DynamicTopAppBar.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/DynamicTopAppBar.kt @@ -18,30 +18,26 @@ import androidx.compose.ui.unit.dp @OptIn(ExperimentalMaterial3Api::class) @Composable fun DynamicTopAppBar(navBarState: NavBarState, modifier: Modifier = Modifier) { - TopAppBar( - modifier = modifier, - colors = TopAppBarDefaults.topAppBarColors().copy(Color.Transparent), - title = { - AnimatedVisibility( - visible = navBarState.showTop, - enter = slideInVertically() + fadeIn(), - exit = slideOutVertically() + fadeOut(), - ) { - Box(modifier = Modifier.padding(start = 10.dp)) { - navBarState.topTitle?.invoke() - } - } - }, - actions = { - AnimatedVisibility( - visible = navBarState.showTop, - enter = slideInVertically() + fadeIn(), - exit = slideOutVertically() + fadeOut(), - ) { - Box(modifier = Modifier.padding(end = 10.dp)) { - navBarState.topTrailing?.invoke() - } - } - }, - ) + TopAppBar( + modifier = modifier, + colors = TopAppBarDefaults.topAppBarColors().copy(Color.Transparent), + title = { + AnimatedVisibility( + visible = navBarState.showTop, + enter = slideInVertically() + fadeIn(), + exit = slideOutVertically() + fadeOut(), + ) { + Box(modifier = Modifier.padding(start = 10.dp)) { navBarState.topTitle?.invoke() } + } + }, + actions = { + AnimatedVisibility( + visible = navBarState.showTop, + enter = slideInVertically() + fadeIn(), + exit = slideOutVertically() + fadeOut(), + ) { + Box(modifier = Modifier.padding(end = 10.dp)) { navBarState.topTrailing?.invoke() } + } + }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/Extensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/Extensions.kt index 23cd41e7..438ea128 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/Extensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/Extensions.kt @@ -9,7 +9,5 @@ import kotlin.reflect.KClass @SuppressLint("RestrictedApi") fun NavBackStackEntry?.isCurrentRoute(cls: KClass): Boolean { - return this?.destination?.hierarchy?.any { - it.hasRoute(route = cls) - } == true + return this?.destination?.hierarchy?.any { it.hasRoute(route = cls) } == true } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/LocalNavController.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/LocalNavController.kt index be5c8062..3854b175 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/LocalNavController.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/LocalNavController.kt @@ -3,6 +3,5 @@ package com.zaneschepke.wireguardautotunnel.ui.common.navigation import androidx.compose.runtime.compositionLocalOf import androidx.navigation.NavHostController -val LocalNavController = compositionLocalOf { - error("NavController was not provided") -} +val LocalNavController = + compositionLocalOf { error("NavController was not provided") } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/NavBarState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/NavBarState.kt index 927ada50..796b72db 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/NavBarState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/NavBarState.kt @@ -29,189 +29,243 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent data class NavBarState( - val showTop: Boolean = true, - val showBottom: Boolean = true, - val topTitle: @Composable (() -> Unit)? = null, - val topTrailing: @Composable (() -> Unit)? = null, - val route: Route? = null, + val showTop: Boolean = true, + val showBottom: Boolean = true, + val topTitle: @Composable (() -> Unit)? = null, + val topTrailing: @Composable (() -> Unit)? = null, + val route: Route? = null, ) @Composable fun currentNavBackStackEntryAsNavBarState( - navController: NavController, - backStackEntry: NavBackStackEntry?, - viewModel: AppViewModel, - uiState: AppUiState, + navController: NavController, + backStackEntry: NavBackStackEntry?, + viewModel: AppViewModel, + uiState: AppUiState, ): State { - return produceState(initialValue = NavBarState(), key1 = backStackEntry, key2 = uiState) { - value = when { - backStackEntry.isCurrentRoute(Route.Main::class) -> { - NavBarState( - showTop = true, showBottom = true, - { Text(stringResource(R.string.tunnels)) }, - { - IconButton( - onClick = { viewModel.handleEvent(AppEvent.ToggleBottomSheet) }, - ) { - val icon = Icons.Rounded.Add - Icon(icon, stringResource(R.string.add_tunnel), modifier = Modifier.size(iconSize)) - } - }, - route = Route.Main, - ) - } - backStackEntry.isCurrentRoute(Route.AutoTunnel::class) -> { - NavBarState( - showTop = true, showBottom = true, - { Text(stringResource(R.string.auto_tunnel)) }, - { - IconButton( - onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnel) }, - ) { - val (icon, description, color) = if (uiState.appSettings.isAutoTunnelEnabled) { - Triple(Icons.Rounded.Stop, R.string.stop_auto, Brick) - } else { - Triple(Icons.Rounded.PlayArrow, R.string.start_auto, SilverTree) - } - Icon(icon, stringResource(description), tint = color, modifier = Modifier.size(iconSize)) - } - }, - route = Route.AutoTunnel, - ) - } - backStackEntry.isCurrentRoute(Route.AutoTunnelAdvanced::class) || backStackEntry.isCurrentRoute(Route.SettingsAdvanced::class) -> { - NavBarState( - showTop = true, showBottom = true, - { Text(stringResource(R.string.advanced_settings)) }, - route = Route.AutoTunnelAdvanced, - ) - } - backStackEntry.isCurrentRoute(Route.Settings::class) -> { - NavBarState( - showTop = true, showBottom = true, - { Text(stringResource(R.string.settings)) }, - { - IconButton( - onClick = { viewModel.handleEvent(AppEvent.ToggleBottomSheet) }, - ) { - val icon = Icons.Rounded.Menu - Icon(icon, stringResource(R.string.quick_actions), modifier = Modifier.size(iconSize)) - } - }, - route = Route.Settings, - ) - } - backStackEntry.isCurrentRoute(Route.KillSwitch::class) -> { - NavBarState( - showTop = true, showBottom = true, - { Text(stringResource(R.string.kill_switch)) }, - route = Route.KillSwitch, - ) - } - backStackEntry.isCurrentRoute(Route.Appearance::class) -> { - NavBarState( - showTop = true, showBottom = true, - { Text(stringResource(R.string.appearance)) }, - route = Route.Appearance, - ) - } - backStackEntry.isCurrentRoute(Route.Language::class) -> { - NavBarState( - showTop = true, showBottom = true, - { Text(stringResource(R.string.language)) }, - route = Route.Language, - ) - } - backStackEntry.isCurrentRoute(Route.Display::class) -> { - NavBarState( - showTop = true, showBottom = true, - { Text(stringResource(R.string.display_theme)) }, - route = Route.Display, - ) - } - backStackEntry.isCurrentRoute(Route.Logs::class) -> { - NavBarState( - showTop = true, showBottom = false, - { Text(stringResource(R.string.logs)) }, - { - IconButton( - onClick = { viewModel.handleEvent(AppEvent.ToggleBottomSheet) }, - ) { - val icon = Icons.Rounded.Menu - Icon(icon, stringResource(R.string.quick_actions), modifier = Modifier.size(iconSize)) - } - }, - route = Route.Logs, - ) - } - backStackEntry.isCurrentRoute(Route.TunnelOptions::class) -> { - val args = backStackEntry?.toRoute() - val tunnel = uiState.tunnels.find { it.id == args?.id } - NavBarState( - showTop = true, showBottom = true, - { tunnel?.name?.let { Text(it) } }, - { - IconButton( - onClick = { tunnel?.id?.let { navController.navigate(Route.Config(id = it)) } }, - ) { - val icon = Icons.Rounded.Edit - Icon(icon, stringResource(R.string.edit_tunnel), modifier = Modifier.size(iconSize)) - } - }, - route = args?.let { Route.TunnelOptions(it.id) }, - ) - } - backStackEntry.isCurrentRoute(Route.SplitTunnel::class) -> { - val args = backStackEntry?.toRoute() - val name = uiState.tunnels.find { it.id == args?.id }?.name - NavBarState( - showTop = true, showBottom = true, - { name?.let { Text(it) } }, - { - IconButton( - onClick = { viewModel.handleEvent(AppEvent.InvokeScreenAction) }, - ) { - val icon = Icons.Rounded.Save - Icon(icon, stringResource(R.string.save), modifier = Modifier.size(iconSize)) - } - }, - route = args?.let { Route.SplitTunnel(it.id) }, - ) - } - backStackEntry.isCurrentRoute(Route.Config::class) -> { - val args = backStackEntry?.toRoute() - val name = uiState.tunnels.find { it.id == args?.id }?.name - NavBarState( - showTop = true, showBottom = true, - { name?.let { Text(it) } }, - { - IconButton( - onClick = { viewModel.handleEvent(AppEvent.InvokeScreenAction) }, - ) { - val icon = Icons.Rounded.Save - Icon(icon, stringResource(R.string.save), modifier = Modifier.size(iconSize)) - } - }, - route = args?.let { Route.Config(it.id) }, - ) - } - backStackEntry.isCurrentRoute(Route.TunnelAutoTunnel::class) -> { - val args = backStackEntry?.toRoute() - val name = uiState.tunnels.find { it.id == args?.id }?.name - NavBarState( - showTop = true, showBottom = true, - { name?.let { Text(it) } }, - route = args?.let { Route.TunnelAutoTunnel(it.id) }, - ) - } - backStackEntry.isCurrentRoute(Route.Support::class) -> { - NavBarState( - showTop = true, showBottom = true, - { Text(stringResource(R.string.support)) }, - route = Route.Support, - ) - } - else -> NavBarState(showTop = false, showBottom = false) - } - } + return produceState(initialValue = NavBarState(), key1 = backStackEntry, key2 = uiState) { + value = + when { + backStackEntry.isCurrentRoute(Route.Main::class) -> { + NavBarState( + showTop = true, + showBottom = true, + { Text(stringResource(R.string.tunnels)) }, + { + IconButton( + onClick = { viewModel.handleEvent(AppEvent.ToggleBottomSheet) } + ) { + val icon = Icons.Rounded.Add + Icon( + icon, + stringResource(R.string.add_tunnel), + modifier = Modifier.size(iconSize), + ) + } + }, + route = Route.Main, + ) + } + backStackEntry.isCurrentRoute(Route.AutoTunnel::class) -> { + NavBarState( + showTop = true, + showBottom = true, + { Text(stringResource(R.string.auto_tunnel)) }, + { + IconButton( + onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnel) } + ) { + val (icon, description, color) = + if (uiState.appSettings.isAutoTunnelEnabled) { + Triple(Icons.Rounded.Stop, R.string.stop_auto, Brick) + } else { + Triple( + Icons.Rounded.PlayArrow, + R.string.start_auto, + SilverTree, + ) + } + Icon( + icon, + stringResource(description), + tint = color, + modifier = Modifier.size(iconSize), + ) + } + }, + route = Route.AutoTunnel, + ) + } + backStackEntry.isCurrentRoute(Route.AutoTunnelAdvanced::class) || + backStackEntry.isCurrentRoute(Route.SettingsAdvanced::class) -> { + NavBarState( + showTop = true, + showBottom = true, + { Text(stringResource(R.string.advanced_settings)) }, + route = Route.AutoTunnelAdvanced, + ) + } + backStackEntry.isCurrentRoute(Route.Settings::class) -> { + NavBarState( + showTop = true, + showBottom = true, + { Text(stringResource(R.string.settings)) }, + { + IconButton( + onClick = { viewModel.handleEvent(AppEvent.ToggleBottomSheet) } + ) { + val icon = Icons.Rounded.Menu + Icon( + icon, + stringResource(R.string.quick_actions), + modifier = Modifier.size(iconSize), + ) + } + }, + route = Route.Settings, + ) + } + backStackEntry.isCurrentRoute(Route.KillSwitch::class) -> { + NavBarState( + showTop = true, + showBottom = true, + { Text(stringResource(R.string.kill_switch)) }, + route = Route.KillSwitch, + ) + } + backStackEntry.isCurrentRoute(Route.Appearance::class) -> { + NavBarState( + showTop = true, + showBottom = true, + { Text(stringResource(R.string.appearance)) }, + route = Route.Appearance, + ) + } + backStackEntry.isCurrentRoute(Route.Language::class) -> { + NavBarState( + showTop = true, + showBottom = true, + { Text(stringResource(R.string.language)) }, + route = Route.Language, + ) + } + backStackEntry.isCurrentRoute(Route.Display::class) -> { + NavBarState( + showTop = true, + showBottom = true, + { Text(stringResource(R.string.display_theme)) }, + route = Route.Display, + ) + } + backStackEntry.isCurrentRoute(Route.Logs::class) -> { + NavBarState( + showTop = true, + showBottom = false, + { Text(stringResource(R.string.logs)) }, + { + IconButton( + onClick = { viewModel.handleEvent(AppEvent.ToggleBottomSheet) } + ) { + val icon = Icons.Rounded.Menu + Icon( + icon, + stringResource(R.string.quick_actions), + modifier = Modifier.size(iconSize), + ) + } + }, + route = Route.Logs, + ) + } + backStackEntry.isCurrentRoute(Route.TunnelOptions::class) -> { + val args = backStackEntry?.toRoute() + val tunnel = uiState.tunnels.find { it.id == args?.id } + NavBarState( + showTop = true, + showBottom = true, + { tunnel?.name?.let { Text(it) } }, + { + IconButton( + onClick = { + tunnel?.id?.let { + navController.navigate(Route.Config(id = it)) + } + } + ) { + val icon = Icons.Rounded.Edit + Icon( + icon, + stringResource(R.string.edit_tunnel), + modifier = Modifier.size(iconSize), + ) + } + }, + route = args?.let { Route.TunnelOptions(it.id) }, + ) + } + backStackEntry.isCurrentRoute(Route.SplitTunnel::class) -> { + val args = backStackEntry?.toRoute() + val name = uiState.tunnels.find { it.id == args?.id }?.name + NavBarState( + showTop = true, + showBottom = true, + { name?.let { Text(it) } }, + { + IconButton( + onClick = { viewModel.handleEvent(AppEvent.InvokeScreenAction) } + ) { + val icon = Icons.Rounded.Save + Icon( + icon, + stringResource(R.string.save), + modifier = Modifier.size(iconSize), + ) + } + }, + route = args?.let { Route.SplitTunnel(it.id) }, + ) + } + backStackEntry.isCurrentRoute(Route.Config::class) -> { + val args = backStackEntry?.toRoute() + val name = uiState.tunnels.find { it.id == args?.id }?.name + NavBarState( + showTop = true, + showBottom = true, + { name?.let { Text(it) } }, + { + IconButton( + onClick = { viewModel.handleEvent(AppEvent.InvokeScreenAction) } + ) { + val icon = Icons.Rounded.Save + Icon( + icon, + stringResource(R.string.save), + modifier = Modifier.size(iconSize), + ) + } + }, + route = args?.let { Route.Config(it.id) }, + ) + } + backStackEntry.isCurrentRoute(Route.TunnelAutoTunnel::class) -> { + val args = backStackEntry?.toRoute() + val name = uiState.tunnels.find { it.id == args?.id }?.name + NavBarState( + showTop = true, + showBottom = true, + { name?.let { Text(it) } }, + route = args?.let { Route.TunnelAutoTunnel(it.id) }, + ) + } + backStackEntry.isCurrentRoute(Route.Support::class) -> { + NavBarState( + showTop = true, + showBottom = true, + { Text(stringResource(R.string.support)) }, + route = Route.Support, + ) + } + else -> NavBarState(showTop = false, showBottom = false) + } + } } 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 6d08c109..df678fcc 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 @@ -13,64 +13,66 @@ import com.zaneschepke.wireguardautotunnel.R @Composable 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_NONE_ENROLLED -> { - onError(context.getString(R.string.bio_not_created)) - false - } + 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_NONE_ENROLLED -> { + onError(context.getString(R.string.bio_not_created)) + false + } - BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> { - onError(context.getString(R.string.bio_update_required)) - false - } + BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> { + onError(context.getString(R.string.bio_update_required)) + false + } - BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED, BiometricManager.BIOMETRIC_STATUS_UNKNOWN, - BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE, BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE, - -> { - onError(context.getString(R.string.bio_not_supported)) - false - } + BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED, + BiometricManager.BIOMETRIC_STATUS_UNKNOWN, + BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE, + BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> { + onError(context.getString(R.string.bio_not_supported)) + false + } - BiometricManager.BIOMETRIC_SUCCESS -> true - else -> false - } - } - if (isBiometricAvailable) { - val executor = remember { ContextCompat.getMainExecutor(context) } + BiometricManager.BIOMETRIC_SUCCESS -> true + else -> false + } + } + if (isBiometricAvailable) { + val executor = remember { ContextCompat.getMainExecutor(context) } - val promptInfo = - BiometricPrompt.PromptInfo.Builder() - .setAllowedAuthenticators(BIOMETRIC_WEAK or DEVICE_CREDENTIAL) - .setTitle(context.getString(R.string.bio_auth_title)) - .setSubtitle(context.getString(R.string.bio_subtitle)) - .build() + val promptInfo = + BiometricPrompt.PromptInfo.Builder() + .setAllowedAuthenticators(BIOMETRIC_WEAK or DEVICE_CREDENTIAL) + .setTitle(context.getString(R.string.bio_auth_title)) + .setSubtitle(context.getString(R.string.bio_subtitle)) + .build() - val biometricPrompt = - BiometricPrompt( - context as FragmentActivity, - executor, - object : BiometricPrompt.AuthenticationCallback() { - override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { - super.onAuthenticationError(errorCode, errString) - onFailure() - } + val biometricPrompt = + BiometricPrompt( + context as FragmentActivity, + executor, + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + onFailure() + } - override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { - super.onAuthenticationSucceeded(result) - onSuccess() - } + override fun onAuthenticationSucceeded( + result: BiometricPrompt.AuthenticationResult + ) { + super.onAuthenticationSucceeded(result) + onSuccess() + } - override fun onAuthenticationFailed() { - super.onAuthenticationFailed() - onFailure() - } - }, - ) - biometricPrompt.authenticate(promptInfo) - } + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + onFailure() + } + }, + ) + biometricPrompt.authenticate(promptInfo) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/snackbar/CustomSnackbar.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/snackbar/CustomSnackbar.kt index 8a797ee3..0d6fa7eb 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/snackbar/CustomSnackbar.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/snackbar/CustomSnackbar.kt @@ -26,38 +26,40 @@ import androidx.compose.ui.unit.dp import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv @Composable -fun CustomSnackBar(message: String, isRtl: Boolean = true, containerColor: Color = MaterialTheme.colorScheme.surface) { - val context = LocalContext.current - Snackbar( - containerColor = containerColor, - modifier = - Modifier - .fillMaxWidth( - if (context.isRunningOnTv()) 1 / 3f else 2 / 3f, - ) - .padding(bottom = 100.dp), - shape = RoundedCornerShape(16.dp), - ) { - CompositionLocalProvider( - LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr, - ) { - Row( - modifier = - Modifier - .width(IntrinsicSize.Max) - .height(IntrinsicSize.Min), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Start, - ) { - val icon = Icons.Rounded.Info - Icon( - icon, - contentDescription = icon.name, - tint = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(end = 10.dp), - ) - Text(message, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(end = 5.dp)) - } - } - } +fun CustomSnackBar( + message: String, + isRtl: Boolean = true, + containerColor: Color = MaterialTheme.colorScheme.surface, +) { + val context = LocalContext.current + Snackbar( + containerColor = containerColor, + modifier = + Modifier.fillMaxWidth(if (context.isRunningOnTv()) 1 / 3f else 2 / 3f) + .padding(bottom = 100.dp), + shape = RoundedCornerShape(16.dp), + ) { + CompositionLocalProvider( + LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr + ) { + Row( + modifier = Modifier.width(IntrinsicSize.Max).height(IntrinsicSize.Min), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + ) { + val icon = Icons.Rounded.Info + Icon( + icon, + contentDescription = icon.name, + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(end = 10.dp), + ) + Text( + message, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(end = 5.dp), + ) + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/text/LogTypeLabel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/text/LogTypeLabel.kt index 1a55f7c8..f9daff1d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/text/LogTypeLabel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/text/LogTypeLabel.kt @@ -13,14 +13,10 @@ import androidx.compose.ui.unit.dp @Composable fun LogTypeLabel(color: Color, content: @Composable () -> Unit) { - Box( - modifier = - Modifier - .size(20.dp) - .clip(RoundedCornerShape(2.dp)) - .background(color), - contentAlignment = Alignment.Center, - ) { - content() - } + Box( + modifier = Modifier.size(20.dp).clip(RoundedCornerShape(2.dp)).background(color), + contentAlignment = Alignment.Center, + ) { + content() + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/textbox/CustomTextField.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/textbox/CustomTextField.kt index 0c8ab4e7..47cb119f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/textbox/CustomTextField.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/textbox/CustomTextField.kt @@ -20,90 +20,93 @@ import androidx.compose.ui.unit.dp @OptIn(ExperimentalMaterial3Api::class) @Composable fun CustomTextField( - value: String, - modifier: Modifier = Modifier, - textStyle: TextStyle = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface), - label: @Composable () -> Unit, - containerColor: Color, - onValueChange: (value: String) -> Unit = {}, - singleLine: Boolean = false, - placeholder: @Composable (() -> Unit)? = null, - keyboardOptions: KeyboardOptions = KeyboardOptions(), - keyboardActions: KeyboardActions = KeyboardActions(), - supportingText: @Composable (() -> Unit)? = null, - leading: @Composable (() -> Unit)? = null, - trailing: @Composable (() -> Unit)? = null, - isError: Boolean = false, - readOnly: Boolean = false, - enabled: Boolean = true, - interactionSource: MutableInteractionSource, + value: String, + modifier: Modifier = Modifier, + textStyle: TextStyle = + MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface), + label: @Composable () -> Unit, + containerColor: Color, + onValueChange: (value: String) -> Unit = {}, + singleLine: Boolean = false, + placeholder: @Composable (() -> Unit)? = null, + keyboardOptions: KeyboardOptions = KeyboardOptions(), + keyboardActions: KeyboardActions = KeyboardActions(), + supportingText: @Composable (() -> Unit)? = null, + leading: @Composable (() -> Unit)? = null, + trailing: @Composable (() -> Unit)? = null, + isError: Boolean = false, + readOnly: Boolean = false, + enabled: Boolean = true, + interactionSource: MutableInteractionSource, ) { - val space = " " - BasicTextField( - value = value, - textStyle = textStyle, - onValueChange = { - onValueChange(it) - }, - keyboardActions = keyboardActions, - keyboardOptions = keyboardOptions, - readOnly = readOnly, - cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface), - modifier = modifier, - interactionSource = interactionSource, - enabled = enabled, - singleLine = singleLine, - ) { - OutlinedTextFieldDefaults.DecorationBox( - value = space + value, - innerTextField = { - if (value.isEmpty()) { - if (placeholder != null) { - placeholder() - } - } - it.invoke() - }, - contentPadding = OutlinedTextFieldDefaults.contentPadding(top = 0.dp, bottom = 0.dp), - leadingIcon = leading, - trailingIcon = trailing, - singleLine = singleLine, - supportingText = supportingText, - colors = TextFieldDefaults.colors().copy( - disabledLabelColor = MaterialTheme.colorScheme.onSurface, - disabledContainerColor = containerColor, - focusedLabelColor = MaterialTheme.colorScheme.onSurface, - focusedContainerColor = containerColor, - unfocusedContainerColor = containerColor, - focusedTextColor = MaterialTheme.colorScheme.onSurface, - cursorColor = MaterialTheme.colorScheme.onSurface, - ), - enabled = enabled, - label = label, - visualTransformation = VisualTransformation.None, - interactionSource = interactionSource, - placeholder = placeholder, - container = { - OutlinedTextFieldDefaults.ContainerBox( - enabled, - isError = isError, - interactionSource, - colors = TextFieldDefaults.colors().copy( - errorContainerColor = containerColor, - disabledLabelColor = MaterialTheme.colorScheme.onSurface, - disabledContainerColor = containerColor, - focusedIndicatorColor = MaterialTheme.colorScheme.onSurface, - focusedLabelColor = MaterialTheme.colorScheme.onSurface, - focusedContainerColor = containerColor, - unfocusedContainerColor = containerColor, - focusedTextColor = MaterialTheme.colorScheme.onSurface, - cursorColor = MaterialTheme.colorScheme.onSurface, - ), - shape = RoundedCornerShape(8.dp), - focusedBorderThickness = 0.5.dp, - unfocusedBorderThickness = 0.5.dp, - ) - }, - ) - } + val space = " " + BasicTextField( + value = value, + textStyle = textStyle, + onValueChange = { onValueChange(it) }, + keyboardActions = keyboardActions, + keyboardOptions = keyboardOptions, + readOnly = readOnly, + cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface), + modifier = modifier, + interactionSource = interactionSource, + enabled = enabled, + singleLine = singleLine, + ) { + OutlinedTextFieldDefaults.DecorationBox( + value = space + value, + innerTextField = { + if (value.isEmpty()) { + if (placeholder != null) { + placeholder() + } + } + it.invoke() + }, + contentPadding = OutlinedTextFieldDefaults.contentPadding(top = 0.dp, bottom = 0.dp), + leadingIcon = leading, + trailingIcon = trailing, + singleLine = singleLine, + supportingText = supportingText, + colors = + TextFieldDefaults.colors() + .copy( + disabledLabelColor = MaterialTheme.colorScheme.onSurface, + disabledContainerColor = containerColor, + focusedLabelColor = MaterialTheme.colorScheme.onSurface, + focusedContainerColor = containerColor, + unfocusedContainerColor = containerColor, + focusedTextColor = MaterialTheme.colorScheme.onSurface, + cursorColor = MaterialTheme.colorScheme.onSurface, + ), + enabled = enabled, + label = label, + visualTransformation = VisualTransformation.None, + interactionSource = interactionSource, + placeholder = placeholder, + container = { + OutlinedTextFieldDefaults.ContainerBox( + enabled, + isError = isError, + interactionSource, + colors = + TextFieldDefaults.colors() + .copy( + errorContainerColor = containerColor, + disabledLabelColor = MaterialTheme.colorScheme.onSurface, + disabledContainerColor = containerColor, + focusedIndicatorColor = MaterialTheme.colorScheme.onSurface, + focusedLabelColor = MaterialTheme.colorScheme.onSurface, + focusedContainerColor = containerColor, + unfocusedContainerColor = containerColor, + focusedTextColor = MaterialTheme.colorScheme.onSurface, + cursorColor = MaterialTheme.colorScheme.onSurface, + ), + shape = RoundedCornerShape(8.dp), + focusedBorderThickness = 0.5.dp, + unfocusedBorderThickness = 0.5.dp, + ) + }, + ) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/enums/InterfaceActions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/enums/InterfaceActions.kt index 9908eb33..7847be33 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/enums/InterfaceActions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/enums/InterfaceActions.kt @@ -1,7 +1,7 @@ package com.zaneschepke.wireguardautotunnel.ui.enums enum class InterfaceActions { - TOGGLE_AMNEZIA_VALUES, - SET_AMNEZIA_COMPATIBILITY, - TOGGLE_SHOW_SCRIPTS, + TOGGLE_AMNEZIA_VALUES, + SET_AMNEZIA_COMPATIBILITY, + TOGGLE_SHOW_SCRIPTS, } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/enums/PeerActions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/enums/PeerActions.kt index dd905edd..8a20712e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/enums/PeerActions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/enums/PeerActions.kt @@ -1,5 +1,5 @@ package com.zaneschepke.wireguardautotunnel.ui.enums enum class PeerActions { - EXCLUDE_LAN, + EXCLUDE_LAN } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/AutoTunnelScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/AutoTunnelScreen.kt index 02789c7c..5b791a02 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/AutoTunnelScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/AutoTunnelScreen.kt @@ -32,81 +32,89 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.Locati import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState import com.zaneschepke.wireguardautotunnel.util.extensions.isLocationServicesEnabled import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv - import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel @OptIn(ExperimentalPermissionsApi::class) @Composable fun AutoTunnelScreen(uiState: AppUiState, viewModel: AppViewModel) { - val context = LocalContext.current - val navController = LocalNavController.current - 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 showLocationDialog by remember { mutableStateOf(false) } + val context = LocalContext.current + val navController = LocalNavController.current + 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 showLocationDialog by remember { mutableStateOf(false) } - fun checkFineLocationGranted() { - isBackgroundLocationGranted = fineLocationState.status.isGranted - } + fun checkFineLocationGranted() { + isBackgroundLocationGranted = fineLocationState.status.isGranted + } - fun isWifiNameReadable(): Boolean { - return when { - !isBackgroundLocationGranted || !fineLocationState.status.isGranted -> { - showLocationDialog = true - false - } - !context.isLocationServicesEnabled() -> { - showLocationServicesAlertDialog = true - false - } - else -> true - } - } + fun isWifiNameReadable(): Boolean { + return when { + !isBackgroundLocationGranted || !fineLocationState.status.isGranted -> { + showLocationDialog = true + false + } + !context.isLocationServicesEnabled() -> { + showLocationServicesAlertDialog = true + false + } + else -> true + } + } - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) checkFineLocationGranted() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - if (context.isRunningOnTv() && Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { - checkFineLocationGranted() - } else { - val backgroundLocationState = rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION) - isBackgroundLocationGranted = backgroundLocationState.status.isGranted - } - } + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) checkFineLocationGranted() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (context.isRunningOnTv() && Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { + checkFineLocationGranted() + } else { + val backgroundLocationState = + rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION) + isBackgroundLocationGranted = backgroundLocationState.status.isGranted + } + } - LaunchedEffect(uiState.appSettings.trustedNetworkSSIDs) { - currentText = "" - } + LaunchedEffect(uiState.appSettings.trustedNetworkSSIDs) { currentText = "" } - LocationServicesDialog( - showLocationServicesAlertDialog, - onDismiss = { showLocationServicesAlertDialog = false }, - onAttest = { showLocationServicesAlertDialog = false }, - ) + LocationServicesDialog( + showLocationServicesAlertDialog, + onDismiss = { showLocationServicesAlertDialog = false }, + onAttest = { showLocationServicesAlertDialog = false }, + ) - BackgroundLocationDialog( - showLocationDialog, - onDismiss = { showLocationDialog = false }, - onAttest = { showLocationDialog = false }, - ) + BackgroundLocationDialog( + showLocationDialog, + onDismiss = { showLocationDialog = false }, + onAttest = { showLocationDialog = false }, + ) - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(vertical = 24.dp) - .padding(horizontal = 24.dp), - ) { - SurfaceSelectionGroupButton( - items = WifiTunnelingItems(uiState, viewModel, currentText, { currentText = it }, { isWifiNameReadable() }), - ) - SurfaceSelectionGroupButton( - items = NetworkTunnelingItems(uiState, viewModel), - ) - SurfaceSelectionGroupButton( - items = listOf(AdvancedSettingsItem(onClick = { navController.navigate(Route.AutoTunnelAdvanced) })), - ) - } + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), + modifier = + Modifier.fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(vertical = 24.dp) + .padding(horizontal = 24.dp), + ) { + SurfaceSelectionGroupButton( + items = + WifiTunnelingItems( + uiState, + viewModel, + currentText, + { currentText = it }, + { isWifiNameReadable() }, + ) + ) + SurfaceSelectionGroupButton(items = NetworkTunnelingItems(uiState, viewModel)) + SurfaceSelectionGroupButton( + items = + listOf( + AdvancedSettingsItem( + onClick = { navController.navigate(Route.AutoTunnelAdvanced) } + ) + ) + ) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/advanced/AutoTunnelAdvancedScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/advanced/AutoTunnelAdvancedScreen.kt index 22bdc739..780539fd 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/advanced/AutoTunnelAdvancedScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/advanced/AutoTunnelAdvancedScreen.kt @@ -12,23 +12,22 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced.components.DebounceDelaySelector import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState - import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel @Composable fun AutoTunnelAdvancedScreen(appUiState: AppUiState, viewModel: AppViewModel) { - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(top = 24.dp) - .padding(horizontal = 24.dp), - ) { - DebounceDelaySelector( - currentDelay = appUiState.appSettings.debounceDelaySeconds, - onEvent = viewModel::handleEvent, - ) - } + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), + modifier = + Modifier.fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(top = 24.dp) + .padding(horizontal = 24.dp), + ) { + DebounceDelaySelector( + currentDelay = appUiState.appSettings.debounceDelaySeconds, + onEvent = viewModel::handleEvent, + ) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/advanced/components/DebounceDelaySelector.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/advanced/components/DebounceDelaySelector.kt index 2dd9348c..1498bc41 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/advanced/components/DebounceDelaySelector.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/advanced/components/DebounceDelaySelector.kt @@ -18,31 +18,32 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable fun DebounceDelaySelector(currentDelay: Int, onEvent: (AppEvent) -> Unit) { - var isDropDownExpanded by remember { mutableStateOf(false) } + var isDropDownExpanded by remember { mutableStateOf(false) } - SurfaceSelectionGroupButton( - listOf( - SelectionItem( - leadingIcon = Icons.Outlined.PauseCircle, - title = { - Text( - stringResource(R.string.debounce_delay), - style = MaterialTheme.typography.bodyMedium.copy( - color = MaterialTheme.colorScheme.onSurface, - ), - ) - }, - onClick = { isDropDownExpanded = true }, - trailing = { - DropdownSelector( - currentValue = currentDelay, - options = (0..10).toList(), - onValueSelected = { num -> onEvent(AppEvent.SetDebounceDelay(num)) }, - isExpanded = isDropDownExpanded, - onDismiss = { isDropDownExpanded = false }, - ) - }, - ), - ), - ) + SurfaceSelectionGroupButton( + listOf( + SelectionItem( + leadingIcon = Icons.Outlined.PauseCircle, + title = { + Text( + stringResource(R.string.debounce_delay), + style = + MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurface + ), + ) + }, + onClick = { isDropDownExpanded = true }, + trailing = { + DropdownSelector( + currentValue = currentDelay, + options = (0..10).toList(), + onValueSelected = { num -> onEvent(AppEvent.SetDebounceDelay(num)) }, + isExpanded = isDropDownExpanded, + onDismiss = { isDropDownExpanded = false }, + ) + }, + ) + ) + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/components/AdvancedSettingsItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/components/AdvancedSettingsItem.kt index 90139781..5f4f930d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/components/AdvancedSettingsItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/components/AdvancedSettingsItem.kt @@ -12,17 +12,16 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionIte @Composable fun AdvancedSettingsItem(onClick: () -> Unit): SelectionItem { - return SelectionItem( - leadingIcon = Icons.Outlined.Settings, - title = { - Text( - stringResource(R.string.advanced_settings), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - trailing = { - ForwardButton { onClick() } - }, - onClick = { onClick() }, - ) + return SelectionItem( + leadingIcon = Icons.Outlined.Settings, + title = { + Text( + stringResource(R.string.advanced_settings), + style = + MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + trailing = { ForwardButton { onClick() } }, + onClick = { onClick() }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/components/NetworkTunnelingItems.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/components/NetworkTunnelingItems.kt index 3b5575e3..7ebb80cf 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/components/NetworkTunnelingItems.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/components/NetworkTunnelingItems.kt @@ -19,80 +19,106 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable fun NetworkTunnelingItems(uiState: AppUiState, viewModel: AppViewModel): List { - return listOf( - SelectionItem( - leadingIcon = Icons.Outlined.SignalCellular4Bar, - title = { - Text( - stringResource(R.string.tunnel_mobile_data), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - trailing = { - ScaledSwitch( - enabled = !uiState.appSettings.isAlwaysOnVpnEnabled, - checked = uiState.appSettings.isTunnelOnMobileDataEnabled, - onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnCellular) }, - ) - }, - description = { - val cellularActive = remember(uiState.networkStatus) { uiState.networkStatus?.cellularConnected ?: false } - Text( - text = if (cellularActive) stringResource(R.string.active) else stringResource(R.string.inactive), - style = MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.outline), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnCellular) }, - ), - SelectionItem( - leadingIcon = Icons.Outlined.SettingsEthernet, - title = { - Text( - stringResource(R.string.tunnel_on_ethernet), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - trailing = { - ScaledSwitch( - enabled = !uiState.appSettings.isAlwaysOnVpnEnabled, - checked = uiState.appSettings.isTunnelOnEthernetEnabled, - onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnEthernet) }, - ) - }, - description = { - val ethernetActive = remember(uiState.networkStatus) { uiState.networkStatus?.ethernetConnected ?: false } - Text( - text = if (ethernetActive) stringResource(R.string.active) else stringResource(R.string.inactive), - style = MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.outline), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnEthernet) }, - ), - SelectionItem( - leadingIcon = Icons.Outlined.AirplanemodeActive, - title = { - Text( - stringResource(R.string.stop_on_no_internet), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - description = { - Text( - stringResource(R.string.stop_on_internet_loss), - style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline), - ) - }, - trailing = { - ScaledSwitch( - checked = uiState.appSettings.isStopOnNoInternetEnabled, - onClick = { viewModel.handleEvent(AppEvent.ToggleStopTunnelOnNoInternet) }, - ) - }, - onClick = { viewModel.handleEvent(AppEvent.ToggleStopTunnelOnNoInternet) }, - ), - ) + return listOf( + SelectionItem( + leadingIcon = Icons.Outlined.SignalCellular4Bar, + title = { + Text( + stringResource(R.string.tunnel_mobile_data), + style = + MaterialTheme.typography.bodyMedium.copy( + MaterialTheme.colorScheme.onSurface + ), + ) + }, + trailing = { + ScaledSwitch( + enabled = !uiState.appSettings.isAlwaysOnVpnEnabled, + checked = uiState.appSettings.isTunnelOnMobileDataEnabled, + onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnCellular) }, + ) + }, + description = { + val cellularActive = + remember(uiState.networkStatus) { + uiState.networkStatus?.cellularConnected ?: false + } + Text( + text = + if (cellularActive) stringResource(R.string.active) + else stringResource(R.string.inactive), + style = + MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.outline + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnCellular) }, + ), + SelectionItem( + leadingIcon = Icons.Outlined.SettingsEthernet, + title = { + Text( + stringResource(R.string.tunnel_on_ethernet), + style = + MaterialTheme.typography.bodyMedium.copy( + MaterialTheme.colorScheme.onSurface + ), + ) + }, + trailing = { + ScaledSwitch( + enabled = !uiState.appSettings.isAlwaysOnVpnEnabled, + checked = uiState.appSettings.isTunnelOnEthernetEnabled, + onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnEthernet) }, + ) + }, + description = { + val ethernetActive = + remember(uiState.networkStatus) { + uiState.networkStatus?.ethernetConnected ?: false + } + Text( + text = + if (ethernetActive) stringResource(R.string.active) + else stringResource(R.string.inactive), + style = + MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.outline + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnEthernet) }, + ), + SelectionItem( + leadingIcon = Icons.Outlined.AirplanemodeActive, + title = { + Text( + stringResource(R.string.stop_on_no_internet), + style = + MaterialTheme.typography.bodyMedium.copy( + MaterialTheme.colorScheme.onSurface + ), + ) + }, + description = { + Text( + stringResource(R.string.stop_on_internet_loss), + style = + MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline), + ) + }, + trailing = { + ScaledSwitch( + checked = uiState.appSettings.isStopOnNoInternetEnabled, + onClick = { viewModel.handleEvent(AppEvent.ToggleStopTunnelOnNoInternet) }, + ) + }, + onClick = { viewModel.handleEvent(AppEvent.ToggleStopTunnelOnNoInternet) }, + ), + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/components/TrustNetworksTextBox.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/components/TrustNetworksTextBox.kt index 69ab9609..b086c29c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/components/TrustNetworksTextBox.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/components/TrustNetworksTextBox.kt @@ -33,79 +33,71 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv @OptIn(ExperimentalLayoutApi::class) @Composable fun TrustedNetworkTextBox( - trustedNetworks: List, - onDelete: (ssid: String) -> Unit, - currentText: String, - onSave: (ssid: String) -> Unit, - onValueChange: (network: String) -> Unit, - supporting: @Composable () -> Unit, + trustedNetworks: List, + onDelete: (ssid: String) -> Unit, + currentText: String, + onSave: (ssid: String) -> Unit, + onValueChange: (network: String) -> Unit, + supporting: @Composable () -> Unit, ) { - val context = LocalContext.current - Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { - FlowRow( - modifier = - Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.CenterHorizontally), - ) { - trustedNetworks.forEach { ssid -> - ClickableIconButton( - onClick = { - if (context.isRunningOnTv()) { - onDelete(ssid) - } - }, - onIconClick = { - onDelete(ssid) - }, - text = ssid, - icon = Icons.Filled.Close, - ) - } - } - CustomTextField( - textStyle = MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.onSurface), - value = currentText, - onValueChange = onValueChange, - interactionSource = remember { MutableInteractionSource() }, - label = { - Text( - stringResource(R.string.add_wifi_name), - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.labelMedium, - ) - }, - containerColor = MaterialTheme.colorScheme.surface, - supportingText = supporting, - modifier = - Modifier - .padding( - top = 5.dp, - bottom = 10.dp, - ).fillMaxWidth().padding(end = 16.dp), - singleLine = true, - keyboardOptions = - KeyboardOptions( - capitalization = KeyboardCapitalization.None, - imeAction = ImeAction.Done, - ), - keyboardActions = KeyboardActions(onDone = { onSave(currentText) }), - trailing = { - if (currentText != "") { - IconButton(onClick = { - onSave(currentText) - }) { - val icon = Icons.Outlined.Add - Icon( - imageVector = icon, - contentDescription = stringResource( - R.string - .trusted_ssid_value_description, - ), - tint = MaterialTheme.colorScheme.primary, - ) - } - } - }, - ) - } + val context = LocalContext.current + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.CenterHorizontally), + ) { + trustedNetworks.forEach { ssid -> + ClickableIconButton( + onClick = { + if (context.isRunningOnTv()) { + onDelete(ssid) + } + }, + onIconClick = { onDelete(ssid) }, + text = ssid, + icon = Icons.Filled.Close, + ) + } + } + CustomTextField( + textStyle = + MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.onSurface + ), + value = currentText, + onValueChange = onValueChange, + interactionSource = remember { MutableInteractionSource() }, + label = { + Text( + stringResource(R.string.add_wifi_name), + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelMedium, + ) + }, + containerColor = MaterialTheme.colorScheme.surface, + supportingText = supporting, + modifier = + Modifier.padding(top = 5.dp, bottom = 10.dp).fillMaxWidth().padding(end = 16.dp), + singleLine = true, + keyboardOptions = + KeyboardOptions( + capitalization = KeyboardCapitalization.None, + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions(onDone = { onSave(currentText) }), + trailing = { + if (currentText != "") { + IconButton(onClick = { onSave(currentText) }) { + val icon = Icons.Outlined.Add + Icon( + imageVector = icon, + contentDescription = + stringResource(R.string.trusted_ssid_value_description), + tint = MaterialTheme.colorScheme.primary, + ) + } + } + }, + ) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/components/WifiTunnelingItems.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/components/WifiTunnelingItems.kt index 3254b046..d9c9463b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/components/WifiTunnelingItems.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/components/WifiTunnelingItems.kt @@ -36,168 +36,201 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LearnM import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl - import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable fun WifiTunnelingItems( - uiState: AppUiState, - viewModel: AppViewModel, - currentText: String, - onTextChange: (String) -> Unit, - isWifiNameReadable: () -> Boolean, + uiState: AppUiState, + viewModel: AppViewModel, + currentText: String, + onTextChange: (String) -> Unit, + isWifiNameReadable: () -> Boolean, ): List { - val context = LocalContext.current - val clipboard = LocalClipboardManager.current + val context = LocalContext.current + val clipboard = LocalClipboardManager.current - val baseItems = listOf( - SelectionItem( - leadingIcon = Icons.Outlined.Wifi, - title = { - Text( - stringResource(R.string.tunnel_on_wifi), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - trailing = { - ScaledSwitch( - enabled = !uiState.appSettings.isAlwaysOnVpnEnabled, - checked = uiState.appSettings.isTunnelOnWifiEnabled, - onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnWifi) }, - ) - }, - description = { - val wifiName by remember(uiState.networkStatus) { - derivedStateOf { - (uiState.networkStatus as? NetworkStatus.Connected) - ?.takeIf { it.wifiConnected } - ?.wifiSsid - } - } - Text( - text = wifiName?.let { stringResource(R.string.wifi_name_template, it) } ?: stringResource(R.string.inactive), - style = MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.outline), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.clickable { - wifiName?.let { clipboard.setText(AnnotatedString(it)) } - }, - ) - }, - onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnWifi) }, - ), - SelectionItem( - leadingIcon = Icons.Outlined.Code, - title = { - Text( - stringResource(R.string.wifi_name_via_shell), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - description = { - Text( - stringResource(R.string.use_root_shell_for_wifi), - style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline), - ) - }, - trailing = { - ScaledSwitch( - checked = uiState.appSettings.isWifiNameByShellEnabled, - onClick = { viewModel.handleEvent(AppEvent.ToggleRootShellWifi) }, - ) - }, - onClick = { viewModel.handleEvent(AppEvent.ToggleRootShellWifi) }, - ), - ) + val baseItems = + listOf( + SelectionItem( + leadingIcon = Icons.Outlined.Wifi, + title = { + Text( + stringResource(R.string.tunnel_on_wifi), + style = + MaterialTheme.typography.bodyMedium.copy( + MaterialTheme.colorScheme.onSurface + ), + ) + }, + trailing = { + ScaledSwitch( + enabled = !uiState.appSettings.isAlwaysOnVpnEnabled, + checked = uiState.appSettings.isTunnelOnWifiEnabled, + onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnWifi) }, + ) + }, + description = { + val wifiName by + remember(uiState.networkStatus) { + derivedStateOf { + (uiState.networkStatus as? NetworkStatus.Connected) + ?.takeIf { it.wifiConnected } + ?.wifiSsid + } + } + Text( + text = + wifiName?.let { stringResource(R.string.wifi_name_template, it) } + ?: stringResource(R.string.inactive), + style = + MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.outline + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = + Modifier.clickable { + wifiName?.let { clipboard.setText(AnnotatedString(it)) } + }, + ) + }, + onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnWifi) }, + ), + SelectionItem( + leadingIcon = Icons.Outlined.Code, + title = { + Text( + stringResource(R.string.wifi_name_via_shell), + style = + MaterialTheme.typography.bodyMedium.copy( + MaterialTheme.colorScheme.onSurface + ), + ) + }, + description = { + Text( + stringResource(R.string.use_root_shell_for_wifi), + style = + MaterialTheme.typography.bodySmall.copy( + MaterialTheme.colorScheme.outline + ), + ) + }, + trailing = { + ScaledSwitch( + checked = uiState.appSettings.isWifiNameByShellEnabled, + onClick = { viewModel.handleEvent(AppEvent.ToggleRootShellWifi) }, + ) + }, + onClick = { viewModel.handleEvent(AppEvent.ToggleRootShellWifi) }, + ), + ) - return if (uiState.appSettings.isTunnelOnWifiEnabled) { - baseItems + listOf( - SelectionItem( - leadingIcon = Icons.Outlined.Filter1, - title = { - Text( - stringResource(R.string.use_wildcards), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - description = { - LearnMoreLinkLabel({ context.openWebUrl(it) }, stringResource(R.string.docs_wildcards)) - }, - trailing = { - ScaledSwitch( - checked = uiState.appSettings.isWildcardsEnabled, - onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelWildcards) }, - ) - }, - onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelWildcards) }, - ), - SelectionItem( - title = { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.weight(4f, false).fillMaxWidth(), - ) { - val icon = Icons.Outlined.Security - Icon( - icon, - icon.name, - modifier = Modifier.size(iconSize), - ) - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically), - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp) - .padding(vertical = 6.dp), - ) { - Text( - stringResource(R.string.trusted_wifi_names), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - } - } - } - }, - description = { - TrustedNetworkTextBox( - uiState.appSettings.trustedNetworkSSIDs, - onDelete = { viewModel.handleEvent(AppEvent.DeleteTrustedSSID(it)) }, - currentText = currentText, - onSave = { ssid -> - if (uiState.appSettings.isWifiNameByShellEnabled || isWifiNameReadable()) { - viewModel.handleEvent(AppEvent.SaveTrustedSSID(ssid)) - } - }, - onValueChange = onTextChange, - supporting = { if (uiState.appSettings.isWildcardsEnabled) WildcardsLabel() }, - ) - }, - ), - SelectionItem( - leadingIcon = Icons.Outlined.VpnKeyOff, - title = { - Text( - stringResource(R.string.kill_switch_off), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - trailing = { - ScaledSwitch( - enabled = uiState.appSettings.isVpnKillSwitchEnabled, - checked = uiState.appSettings.isDisableKillSwitchOnTrustedEnabled, - onClick = { viewModel.handleEvent(AppEvent.ToggleStopKillSwitchOnTrusted) }, - ) - }, - onClick = { viewModel.handleEvent(AppEvent.ToggleStopKillSwitchOnTrusted) }, - ), - ) - } else { - baseItems - } + return if (uiState.appSettings.isTunnelOnWifiEnabled) { + baseItems + + listOf( + SelectionItem( + leadingIcon = Icons.Outlined.Filter1, + title = { + Text( + stringResource(R.string.use_wildcards), + style = + MaterialTheme.typography.bodyMedium.copy( + MaterialTheme.colorScheme.onSurface + ), + ) + }, + description = { + LearnMoreLinkLabel( + { context.openWebUrl(it) }, + stringResource(R.string.docs_wildcards), + ) + }, + trailing = { + ScaledSwitch( + checked = uiState.appSettings.isWildcardsEnabled, + onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelWildcards) }, + ) + }, + onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelWildcards) }, + ), + SelectionItem( + title = { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.weight(4f, false).fillMaxWidth(), + ) { + val icon = Icons.Outlined.Security + Icon(icon, icon.name, modifier = Modifier.size(iconSize)) + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = + Arrangement.spacedBy(2.dp, Alignment.CenterVertically), + modifier = + Modifier.fillMaxWidth() + .padding(start = 16.dp) + .padding(vertical = 6.dp), + ) { + Text( + stringResource(R.string.trusted_wifi_names), + style = + MaterialTheme.typography.bodyMedium.copy( + MaterialTheme.colorScheme.onSurface + ), + ) + } + } + } + }, + description = { + TrustedNetworkTextBox( + uiState.appSettings.trustedNetworkSSIDs, + onDelete = { viewModel.handleEvent(AppEvent.DeleteTrustedSSID(it)) }, + currentText = currentText, + onSave = { ssid -> + if ( + uiState.appSettings.isWifiNameByShellEnabled || + isWifiNameReadable() + ) { + viewModel.handleEvent(AppEvent.SaveTrustedSSID(ssid)) + } + }, + onValueChange = onTextChange, + supporting = { + if (uiState.appSettings.isWildcardsEnabled) WildcardsLabel() + }, + ) + }, + ), + SelectionItem( + leadingIcon = Icons.Outlined.VpnKeyOff, + title = { + Text( + stringResource(R.string.kill_switch_off), + style = + MaterialTheme.typography.bodyMedium.copy( + MaterialTheme.colorScheme.onSurface + ), + ) + }, + trailing = { + ScaledSwitch( + enabled = uiState.appSettings.isVpnKillSwitchEnabled, + checked = uiState.appSettings.isDisableKillSwitchOnTrustedEnabled, + onClick = { + viewModel.handleEvent(AppEvent.ToggleStopKillSwitchOnTrusted) + }, + ) + }, + onClick = { viewModel.handleEvent(AppEvent.ToggleStopKillSwitchOnTrusted) }, + ), + ) + } else { + baseItems + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/components/WildcardsLabel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/components/WildcardsLabel.kt index 0931d0d1..5788c2f9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/components/WildcardsLabel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/components/WildcardsLabel.kt @@ -9,8 +9,12 @@ import com.zaneschepke.wireguardautotunnel.R @Composable fun WildcardsLabel() { - Text( - stringResource(R.string.wildcards_active), - style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline, fontStyle = FontStyle.Italic), - ) + Text( + stringResource(R.string.wildcards_active), + style = + MaterialTheme.typography.bodySmall.copy( + MaterialTheme.colorScheme.outline, + fontStyle = FontStyle.Italic, + ), + ) } 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 7500d01d..6077f21c 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 @@ -31,81 +31,98 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable fun MainScreen(appUiState: AppUiState, appViewState: AppViewState, viewModel: AppViewModel) { - val navController = LocalNavController.current - val clipboard = LocalClipboardManager.current + val navController = LocalNavController.current + val clipboard = LocalClipboardManager.current - var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) } - var showUrlImportDialog by remember { mutableStateOf(false) } + var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) } + var showUrlImportDialog by remember { mutableStateOf(false) } - val tunnelFileImportResultLauncher = rememberFileImportLauncherForResult( - onNoFileExplorer = { viewModel.handleEvent(AppEvent.ShowMessage(StringValue.StringResource(R.string.error_no_file_explorer))) }, - onData = { data -> viewModel.handleEvent(AppEvent.ImportTunnelFromFile(data)) }, - ) + val tunnelFileImportResultLauncher = + rememberFileImportLauncherForResult( + onNoFileExplorer = { + viewModel.handleEvent( + AppEvent.ShowMessage( + StringValue.StringResource(R.string.error_no_file_explorer) + ) + ) + }, + onData = { data -> viewModel.handleEvent(AppEvent.ImportTunnelFromFile(data)) }, + ) - val requestPermissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> - if (!isGranted) { - viewModel.handleEvent( - AppEvent.ShowMessage(StringValue.StringResource(R.string.camera_permission_required)), - ) - return@rememberLauncherForActivityResult - } - navController.navigate(Route.Scanner) - } + val requestPermissionLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted + -> + if (!isGranted) { + viewModel.handleEvent( + AppEvent.ShowMessage( + StringValue.StringResource(R.string.camera_permission_required) + ) + ) + return@rememberLauncherForActivityResult + } + navController.navigate(Route.Scanner) + } - if (showDeleteTunnelAlertDialog && appViewState.selectedTunnel != null) { - InfoDialog( - onDismiss = { showDeleteTunnelAlertDialog = false }, - onAttest = { - appViewState.selectedTunnel.let { viewModel.handleEvent(AppEvent.DeleteTunnel(it)) } - showDeleteTunnelAlertDialog = false - viewModel.handleEvent(AppEvent.SetSelectedTunnel(null)) - }, - title = { Text(text = stringResource(R.string.delete_tunnel)) }, - body = { Text(text = stringResource(R.string.delete_tunnel_message)) }, - confirmText = { Text(text = stringResource(R.string.yes)) }, - ) - } + if (showDeleteTunnelAlertDialog && appViewState.selectedTunnel != null) { + InfoDialog( + onDismiss = { showDeleteTunnelAlertDialog = false }, + onAttest = { + appViewState.selectedTunnel.let { viewModel.handleEvent(AppEvent.DeleteTunnel(it)) } + showDeleteTunnelAlertDialog = false + viewModel.handleEvent(AppEvent.SetSelectedTunnel(null)) + }, + title = { Text(text = stringResource(R.string.delete_tunnel)) }, + body = { Text(text = stringResource(R.string.delete_tunnel_message)) }, + confirmText = { Text(text = stringResource(R.string.yes)) }, + ) + } - TunnelImportSheet( - appViewState.showBottomSheet, - onDismiss = { viewModel.handleEvent(AppEvent.ToggleBottomSheet) }, - onFileClick = { tunnelFileImportResultLauncher.launch(Constants.ALLOWED_TV_FILE_TYPES) }, - onQrClick = { requestPermissionLauncher.launch(android.Manifest.permission.CAMERA) }, - onClipboardClick = { clipboard.getText()?.text?.let { viewModel.handleEvent(AppEvent.ImportTunnelFromClipboard(it)) } }, - onManualImportClick = { navController.navigate(Route.Config(Constants.MANUAL_TUNNEL_CONFIG_ID)) }, - onUrlClick = { showUrlImportDialog = true }, - ) + TunnelImportSheet( + appViewState.showBottomSheet, + onDismiss = { viewModel.handleEvent(AppEvent.ToggleBottomSheet) }, + onFileClick = { tunnelFileImportResultLauncher.launch(Constants.ALLOWED_TV_FILE_TYPES) }, + onQrClick = { requestPermissionLauncher.launch(android.Manifest.permission.CAMERA) }, + onClipboardClick = { + clipboard.getText()?.text?.let { + viewModel.handleEvent(AppEvent.ImportTunnelFromClipboard(it)) + } + }, + onManualImportClick = { + navController.navigate(Route.Config(Constants.MANUAL_TUNNEL_CONFIG_ID)) + }, + onUrlClick = { showUrlImportDialog = true }, + ) - if (showUrlImportDialog) { - UrlImportDialog( - onDismiss = { showUrlImportDialog = false }, - onConfirm = { url -> - viewModel.handleEvent(AppEvent.ImportTunnelFromUrl(url)) - showUrlImportDialog = false - }, - ) - } + if (showUrlImportDialog) { + UrlImportDialog( + onDismiss = { showUrlImportDialog = false }, + onConfirm = { url -> + viewModel.handleEvent(AppEvent.ImportTunnelFromUrl(url)) + showUrlImportDialog = false + }, + ) + } - TunnelList( - appUiState = appUiState, - activeTunnels = appUiState.activeTunnels, - selectedTunnel = appViewState.selectedTunnel, - onSetSelectedTunnel = { viewModel.handleEvent(AppEvent.SetSelectedTunnel(it)) }, - onDeleteTunnel = { - viewModel.handleEvent(AppEvent.SetSelectedTunnel(it)) - showDeleteTunnelAlertDialog = true - }, - onToggleTunnel = { tunnel, checked -> - if (checked) viewModel.handleEvent(AppEvent.StartTunnel(tunnel)) else viewModel.handleEvent(AppEvent.StopTunnel(tunnel)) - }, - onExpandStats = { viewModel.handleEvent(AppEvent.ToggleTunnelStatsExpanded) }, - onCopyTunnel = { - viewModel.handleEvent(AppEvent.CopyTunnel(it)) - viewModel.handleEvent(AppEvent.SetSelectedTunnel(null)) - }, - modifier = Modifier - .fillMaxSize() - .padding(top = 12.dp, bottom = 24.dp).padding(horizontal = 12.dp), - viewModel = viewModel, - ) + TunnelList( + appUiState = appUiState, + activeTunnels = appUiState.activeTunnels, + selectedTunnel = appViewState.selectedTunnel, + onSetSelectedTunnel = { viewModel.handleEvent(AppEvent.SetSelectedTunnel(it)) }, + onDeleteTunnel = { + viewModel.handleEvent(AppEvent.SetSelectedTunnel(it)) + showDeleteTunnelAlertDialog = true + }, + onToggleTunnel = { tunnel, checked -> + if (checked) viewModel.handleEvent(AppEvent.StartTunnel(tunnel)) + else viewModel.handleEvent(AppEvent.StopTunnel(tunnel)) + }, + onExpandStats = { viewModel.handleEvent(AppEvent.ToggleTunnelStatsExpanded) }, + onCopyTunnel = { + viewModel.handleEvent(AppEvent.CopyTunnel(it)) + viewModel.handleEvent(AppEvent.SetSelectedTunnel(null)) + }, + modifier = + Modifier.fillMaxSize().padding(top = 12.dp, bottom = 24.dp).padding(horizontal = 12.dp), + viewModel = viewModel, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/autotunnel/TunnelAutoTunnelScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/autotunnel/TunnelAutoTunnelScreen.kt index 9af86d56..fae73222 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/autotunnel/TunnelAutoTunnelScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/autotunnel/TunnelAutoTunnelScreen.kt @@ -24,28 +24,35 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.components import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel @Composable -fun TunnelAutoTunnelScreen(tunnelConf: TunnelConf, appSettings: AppSettings, viewModel: AppViewModel) { - var currentText by remember { mutableStateOf("") } +fun TunnelAutoTunnelScreen( + tunnelConf: TunnelConf, + appSettings: AppSettings, + viewModel: AppViewModel, +) { + var currentText by remember { mutableStateOf("") } - LaunchedEffect(tunnelConf.tunnelNetworks) { - currentText = "" - } + LaunchedEffect(tunnelConf.tunnelNetworks) { currentText = "" } - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(top = 24.dp) - .padding(horizontal = 24.dp), - ) { - SurfaceSelectionGroupButton( - items = buildList { - add(MobileDataTunnelItem(tunnelConf, viewModel)) - add(EthernetTunnelItem(tunnelConf, viewModel)) - add(WifiTunnelItem(tunnelConf, appSettings, viewModel, currentText) { currentText = it }) - }, - ) - } + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), + modifier = + Modifier.fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(top = 24.dp) + .padding(horizontal = 24.dp), + ) { + SurfaceSelectionGroupButton( + items = + buildList { + add(MobileDataTunnelItem(tunnelConf, viewModel)) + add(EthernetTunnelItem(tunnelConf, viewModel)) + add( + WifiTunnelItem(tunnelConf, appSettings, viewModel, currentText) { + currentText = it + } + ) + } + ) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/autotunnel/components/EthernetTunnelItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/autotunnel/components/EthernetTunnelItem.kt index 9905c9af..8250ae0e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/autotunnel/components/EthernetTunnelItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/autotunnel/components/EthernetTunnelItem.kt @@ -15,26 +15,27 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable fun EthernetTunnelItem(tunnelConf: TunnelConf, viewModel: AppViewModel): SelectionItem { - return SelectionItem( - leadingIcon = Icons.Outlined.SettingsEthernet, - title = { - Text( - text = stringResource(R.string.ethernet_tunnel), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - description = { - Text( - text = stringResource(R.string.set_ethernet_tunnel), - style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline), - ) - }, - trailing = { - ScaledSwitch( - checked = tunnelConf.isEthernetTunnel, - onClick = { viewModel.handleEvent(AppEvent.ToggleEthernetTunnel(tunnelConf)) }, - ) - }, - onClick = { viewModel.handleEvent(AppEvent.ToggleEthernetTunnel(tunnelConf)) }, - ) + return SelectionItem( + leadingIcon = Icons.Outlined.SettingsEthernet, + title = { + Text( + text = stringResource(R.string.ethernet_tunnel), + style = + MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + description = { + Text( + text = stringResource(R.string.set_ethernet_tunnel), + style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline), + ) + }, + trailing = { + ScaledSwitch( + checked = tunnelConf.isEthernetTunnel, + onClick = { viewModel.handleEvent(AppEvent.ToggleEthernetTunnel(tunnelConf)) }, + ) + }, + onClick = { viewModel.handleEvent(AppEvent.ToggleEthernetTunnel(tunnelConf)) }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/autotunnel/components/MobileDataTunnelItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/autotunnel/components/MobileDataTunnelItem.kt index 3038a599..cefdc34b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/autotunnel/components/MobileDataTunnelItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/autotunnel/components/MobileDataTunnelItem.kt @@ -15,26 +15,27 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable fun MobileDataTunnelItem(tunnelConf: TunnelConf, viewModel: AppViewModel): SelectionItem { - return SelectionItem( - leadingIcon = Icons.Outlined.PhoneAndroid, - title = { - Text( - text = stringResource(R.string.mobile_tunnel), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - description = { - Text( - text = stringResource(R.string.mobile_data_tunnel), - style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline), - ) - }, - trailing = { - ScaledSwitch( - checked = tunnelConf.isMobileDataTunnel, - onClick = { viewModel.handleEvent(AppEvent.ToggleMobileDataTunnel(tunnelConf)) }, - ) - }, - onClick = { viewModel.handleEvent(AppEvent.ToggleMobileDataTunnel(tunnelConf)) }, - ) + return SelectionItem( + leadingIcon = Icons.Outlined.PhoneAndroid, + title = { + Text( + text = stringResource(R.string.mobile_tunnel), + style = + MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + description = { + Text( + text = stringResource(R.string.mobile_data_tunnel), + style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline), + ) + }, + trailing = { + ScaledSwitch( + checked = tunnelConf.isMobileDataTunnel, + onClick = { viewModel.handleEvent(AppEvent.ToggleMobileDataTunnel(tunnelConf)) }, + ) + }, + onClick = { viewModel.handleEvent(AppEvent.ToggleMobileDataTunnel(tunnelConf)) }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/autotunnel/components/WifiTunnelItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/autotunnel/components/WifiTunnelItem.kt index b7884bb3..ae65f582 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/autotunnel/components/WifiTunnelItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/autotunnel/components/WifiTunnelItem.kt @@ -27,50 +27,50 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable fun WifiTunnelItem( - tunnelConf: TunnelConf, - appSettings: AppSettings, - viewModel: AppViewModel, - currentText: String, - onTextChange: (String) -> Unit, + tunnelConf: TunnelConf, + appSettings: AppSettings, + viewModel: AppViewModel, + currentText: String, + onTextChange: (String) -> Unit, ): SelectionItem { - return SelectionItem( - title = { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Start, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), - ) { - Icon( - imageVector = Icons.Outlined.Security, - contentDescription = stringResource(R.string.use_tunnel_on_wifi_name), - modifier = Modifier.size(iconSize), - ) - Text( - text = stringResource(R.string.use_tunnel_on_wifi_name), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - modifier = Modifier - .padding(start = 16.dp), - ) - } - }, - description = { - TrustedNetworkTextBox( - trustedNetworks = tunnelConf.tunnelNetworks, - onDelete = { viewModel.handleEvent(AppEvent.DeleteTunnelRunSSID(it, tunnelConf)) }, - currentText = currentText, - onSave = { - viewModel.handleEvent(AppEvent.AddTunnelRunSSID(it, tunnelConf)) - onTextChange("") // Reset the text field after saving - }, - onValueChange = onTextChange, - supporting = { - if (appSettings.isWildcardsEnabled) { - WildcardsLabel() - } - }, - ) - }, - ) + return SelectionItem( + title = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), + ) { + Icon( + imageVector = Icons.Outlined.Security, + contentDescription = stringResource(R.string.use_tunnel_on_wifi_name), + modifier = Modifier.size(iconSize), + ) + Text( + text = stringResource(R.string.use_tunnel_on_wifi_name), + style = + MaterialTheme.typography.bodyMedium.copy( + MaterialTheme.colorScheme.onSurface + ), + modifier = Modifier.padding(start = 16.dp), + ) + } + }, + description = { + TrustedNetworkTextBox( + trustedNetworks = tunnelConf.tunnelNetworks, + onDelete = { viewModel.handleEvent(AppEvent.DeleteTunnelRunSSID(it, tunnelConf)) }, + currentText = currentText, + onSave = { + viewModel.handleEvent(AppEvent.AddTunnelRunSSID(it, tunnelConf)) + onTextChange("") // Reset the text field after saving + }, + onValueChange = onTextChange, + supporting = { + if (appSettings.isWildcardsEnabled) { + WildcardsLabel() + } + }, + ) + }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/GettingStartedLabel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/GettingStartedLabel.kt index 937239f3..bb16c43f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/GettingStartedLabel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/GettingStartedLabel.kt @@ -21,51 +21,40 @@ import com.zaneschepke.wireguardautotunnel.R @Composable fun GettingStartedLabel(onClick: (url: String) -> Unit) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = - Modifier - .padding(top = 100.dp) - .fillMaxSize(), - ) { - val gettingStarted = - buildAnnotatedString { - append(stringResource(id = R.string.see_the)) - append(" ") - pushStringAnnotation( - tag = "gettingStarted", - annotation = stringResource(id = R.string.getting_started_url), - ) - withStyle( - style = SpanStyle(color = MaterialTheme.colorScheme.primary), - ) { - append(stringResource(id = R.string.getting_started_guide)) - } - pop() - append(" ") - append(stringResource(R.string.unsure_how)) - append(".") - } - Text( - text = stringResource(R.string.no_tunnels), - fontStyle = FontStyle.Italic, - ) - ClickableText( - modifier = - Modifier - .padding(vertical = 10.dp, horizontal = 24.dp), - text = gettingStarted, - style = - MaterialTheme.typography.bodyMedium.copy( - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - ), - ) { - gettingStarted.getStringAnnotations(tag = "gettingStarted", it, it) - .firstOrNull()?.let { annotation -> - onClick(annotation.item) - } - } - } + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.padding(top = 100.dp).fillMaxSize(), + ) { + val gettingStarted = buildAnnotatedString { + append(stringResource(id = R.string.see_the)) + append(" ") + pushStringAnnotation( + tag = "gettingStarted", + annotation = stringResource(id = R.string.getting_started_url), + ) + withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) { + append(stringResource(id = R.string.getting_started_guide)) + } + pop() + append(" ") + append(stringResource(R.string.unsure_how)) + append(".") + } + Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic) + ClickableText( + modifier = Modifier.padding(vertical = 10.dp, horizontal = 24.dp), + text = gettingStarted, + style = + MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ), + ) { + gettingStarted + .getStringAnnotations(tag = "gettingStarted", it, it) + .firstOrNull() + ?.let { annotation -> onClick(annotation.item) } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/ScrollDismissMultiFab.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/ScrollDismissMultiFab.kt index b2cf6c92..cd0d26a8 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/ScrollDismissMultiFab.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/ScrollDismissMultiFab.kt @@ -13,22 +13,18 @@ import androidx.compose.ui.unit.dp @Composable fun ScrollDismissFab(icon: @Composable () -> Unit, isVisible: Boolean, onClick: () -> Unit) { - AnimatedVisibility( - visible = isVisible, - enter = slideInVertically(initialOffsetY = { it * 2 }), - exit = slideOutVertically(targetOffsetY = { it * 2 }), - modifier = - Modifier - .focusGroup(), - ) { - FloatingActionButton( - onClick = { - onClick() - }, - shape = RoundedCornerShape(16.dp), - containerColor = MaterialTheme.colorScheme.primary, - ) { - icon() - } - } + AnimatedVisibility( + visible = isVisible, + enter = slideInVertically(initialOffsetY = { it * 2 }), + exit = slideOutVertically(targetOffsetY = { it * 2 }), + modifier = Modifier.focusGroup(), + ) { + FloatingActionButton( + onClick = { onClick() }, + shape = RoundedCornerShape(16.dp), + containerColor = MaterialTheme.colorScheme.primary, + ) { + icon() + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelImportSheet.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelImportSheet.kt index 06177cf6..376efb18 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelImportSheet.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelImportSheet.kt @@ -29,132 +29,118 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv @OptIn(ExperimentalMaterial3Api::class) @Composable fun TunnelImportSheet( - show: Boolean, - onDismiss: () -> Unit, - onFileClick: () -> Unit, - onQrClick: () -> Unit, - onManualImportClick: () -> Unit, - onClipboardClick: () -> Unit, - onUrlClick: () -> Unit, + show: Boolean, + onDismiss: () -> Unit, + onFileClick: () -> Unit, + onQrClick: () -> Unit, + onManualImportClick: () -> Unit, + onClipboardClick: () -> Unit, + onUrlClick: () -> Unit, ) { - val sheetState = rememberModalBottomSheetState() + val sheetState = rememberModalBottomSheetState() - val context = LocalContext.current - if (show) { - ModalBottomSheet( - containerColor = MaterialTheme.colorScheme.surface, - onDismissRequest = { - onDismiss() - }, - sheetState = sheetState, - ) { - Row( - modifier = - Modifier - .fillMaxWidth() - .clickable { - onDismiss() - onFileClick() - } - .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 (!context.isRunningOnTv()) { - HorizontalDivider() - Row( - modifier = - Modifier - .fillMaxWidth() - .clickable { - onDismiss() - onQrClick() - } - .padding(10.dp), - ) { - Icon( - Icons.Filled.QrCode, - contentDescription = stringResource(id = R.string.qr_scan), - modifier = Modifier.padding(10.dp), - ) - Text( - stringResource(id = R.string.add_from_qr), - modifier = Modifier.padding(10.dp), - ) - } - HorizontalDivider() - Row( - modifier = - Modifier - .fillMaxWidth() - .clickable { - onDismiss() - onClipboardClick() - } - .padding(10.dp), - ) { - val icon = Icons.Filled.ContentPasteGo - Icon( - icon, - contentDescription = icon.name, - modifier = Modifier.padding(10.dp), - ) - Text( - stringResource(id = R.string.add_from_clipboard), - modifier = Modifier.padding(10.dp), - ) - } - } - HorizontalDivider() - Row( - modifier = - Modifier - .fillMaxWidth() - .clickable { - onDismiss() - onUrlClick() - } - .padding(10.dp), - ) { - Icon( - Icons.Filled.Link, - contentDescription = stringResource(id = R.string.add_from_url), - modifier = Modifier.padding(10.dp), - ) - Text( - stringResource(id = R.string.add_from_url), - modifier = Modifier.padding(10.dp), - ) - } - HorizontalDivider() - Row( - modifier = - Modifier - .fillMaxWidth() - .clickable { - onDismiss() - onManualImportClick() - } - .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), - ) - } - } - } + val context = LocalContext.current + if (show) { + ModalBottomSheet( + containerColor = MaterialTheme.colorScheme.surface, + onDismissRequest = { onDismiss() }, + sheetState = sheetState, + ) { + Row( + modifier = + Modifier.fillMaxWidth() + .clickable { + onDismiss() + onFileClick() + } + .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 (!context.isRunningOnTv()) { + HorizontalDivider() + Row( + modifier = + Modifier.fillMaxWidth() + .clickable { + onDismiss() + onQrClick() + } + .padding(10.dp) + ) { + Icon( + Icons.Filled.QrCode, + contentDescription = stringResource(id = R.string.qr_scan), + modifier = Modifier.padding(10.dp), + ) + Text( + stringResource(id = R.string.add_from_qr), + modifier = Modifier.padding(10.dp), + ) + } + HorizontalDivider() + Row( + modifier = + Modifier.fillMaxWidth() + .clickable { + onDismiss() + onClipboardClick() + } + .padding(10.dp) + ) { + val icon = Icons.Filled.ContentPasteGo + Icon(icon, contentDescription = icon.name, modifier = Modifier.padding(10.dp)) + Text( + stringResource(id = R.string.add_from_clipboard), + modifier = Modifier.padding(10.dp), + ) + } + } + HorizontalDivider() + Row( + modifier = + Modifier.fillMaxWidth() + .clickable { + onDismiss() + onUrlClick() + } + .padding(10.dp) + ) { + Icon( + Icons.Filled.Link, + contentDescription = stringResource(id = R.string.add_from_url), + modifier = Modifier.padding(10.dp), + ) + Text(stringResource(id = R.string.add_from_url), modifier = Modifier.padding(10.dp)) + } + HorizontalDivider() + Row( + modifier = + Modifier.fillMaxWidth() + .clickable { + onDismiss() + onManualImportClick() + } + .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), + ) + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelList.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelList.kt index f7a7bbd5..de2eaafb 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelList.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelList.kt @@ -26,55 +26,54 @@ import java.util.* @OptIn(ExperimentalFoundationApi::class) @Composable fun TunnelList( - appUiState: AppUiState, - activeTunnels: Map, - selectedTunnel: TunnelConf?, - onSetSelectedTunnel: (TunnelConf?) -> Unit, - onDeleteTunnel: (TunnelConf) -> Unit, - onToggleTunnel: (TunnelConf, Boolean) -> Unit, - onExpandStats: () -> Unit, - onCopyTunnel: (TunnelConf) -> Unit, - modifier: Modifier = Modifier, - viewModel: AppViewModel, + appUiState: AppUiState, + activeTunnels: Map, + selectedTunnel: TunnelConf?, + onSetSelectedTunnel: (TunnelConf?) -> Unit, + onDeleteTunnel: (TunnelConf) -> Unit, + onToggleTunnel: (TunnelConf, Boolean) -> Unit, + onExpandStats: () -> Unit, + onCopyTunnel: (TunnelConf) -> Unit, + modifier: Modifier = Modifier, + viewModel: AppViewModel, ) { - val context = LocalContext.current - val collator = Collator.getInstance(Locale.getDefault()) - val sortedTunnels = remember(appUiState.tunnels) { - appUiState.tunnels.sortedWith(compareBy(collator) { it.tunName }) - } + val context = LocalContext.current + val collator = Collator.getInstance(Locale.getDefault()) + val sortedTunnels = + remember(appUiState.tunnels) { + appUiState.tunnels.sortedWith(compareBy(collator) { it.tunName }) + } - LazyColumn( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(5.dp, Alignment.Top), - modifier = modifier - .pointerInput(Unit) { - if (appUiState.tunnels.isEmpty()) return@pointerInput - }.overscroll(ScrollableDefaults.overscrollEffect()), - state = rememberLazyListState(0, appUiState.tunnels.count()), - userScrollEnabled = true, - reverseLayout = false, - flingBehavior = ScrollableDefaults.flingBehavior(), - ) { - if (appUiState.tunnels.isEmpty()) { - item { - GettingStartedLabel(onClick = { context.openWebUrl(it) }) - } - } - items(sortedTunnels, key = { it.id }) { tunnel -> - val tunnelState = activeTunnels.getValueById(tunnel.id) ?: TunnelState() - TunnelRowItem( - isActive = tunnelState.status.isUpOrStarting(), - expanded = appUiState.appState.isTunnelStatsExpanded, - isSelected = selectedTunnel?.id == tunnel.id, - tunnel = tunnel, - tunnelState = tunnelState, - onSetSelectedTunnel = { onSetSelectedTunnel(it) }, - onClick = onExpandStats, - onCopy = { onCopyTunnel(tunnel) }, - onDelete = { onDeleteTunnel(tunnel) }, - onSwitchClick = { checked -> onToggleTunnel(tunnel, checked) }, - viewModel = viewModel, - ) - } - } + LazyColumn( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(5.dp, Alignment.Top), + modifier = + modifier + .pointerInput(Unit) { if (appUiState.tunnels.isEmpty()) return@pointerInput } + .overscroll(ScrollableDefaults.overscrollEffect()), + state = rememberLazyListState(0, appUiState.tunnels.count()), + userScrollEnabled = true, + reverseLayout = false, + flingBehavior = ScrollableDefaults.flingBehavior(), + ) { + if (appUiState.tunnels.isEmpty()) { + item { GettingStartedLabel(onClick = { context.openWebUrl(it) }) } + } + items(sortedTunnels, key = { it.id }) { tunnel -> + val tunnelState = activeTunnels.getValueById(tunnel.id) ?: TunnelState() + TunnelRowItem( + isActive = tunnelState.status.isUpOrStarting(), + expanded = appUiState.appState.isTunnelStatsExpanded, + isSelected = selectedTunnel?.id == tunnel.id, + tunnel = tunnel, + tunnelState = tunnelState, + onSetSelectedTunnel = { onSetSelectedTunnel(it) }, + onClick = onExpandStats, + onCopy = { onCopyTunnel(tunnel) }, + onDelete = { onDeleteTunnel(tunnel) }, + onSwitchClick = { checked -> onToggleTunnel(tunnel, checked) }, + viewModel = viewModel, + ) + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelRowItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelRowItem.kt index 9baf3fea..35bc3cbd 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelRowItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelRowItem.kt @@ -41,180 +41,182 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable fun TunnelRowItem( - isActive: Boolean, - expanded: Boolean, - isSelected: Boolean, - tunnel: TunnelConf, - tunnelState: TunnelState, - onSetSelectedTunnel: (TunnelConf?) -> Unit, - onClick: () -> Unit, - onCopy: () -> Unit, - onDelete: () -> Unit, - onSwitchClick: (Boolean) -> Unit, - viewModel: AppViewModel, + isActive: Boolean, + expanded: Boolean, + isSelected: Boolean, + tunnel: TunnelConf, + tunnelState: TunnelState, + onSetSelectedTunnel: (TunnelConf?) -> Unit, + onClick: () -> Unit, + onCopy: () -> Unit, + onDelete: () -> Unit, + onSwitchClick: (Boolean) -> Unit, + viewModel: AppViewModel, ) { - val context = LocalContext.current - val navController = LocalNavController.current - val haptic = LocalHapticFeedback.current - val itemFocusRequester = remember { FocusRequester() } - val isTv = context.isRunningOnTv() + val context = LocalContext.current + val navController = LocalNavController.current + val haptic = LocalHapticFeedback.current + val itemFocusRequester = remember { FocusRequester() } + val isTv = context.isRunningOnTv() - val leadingIconColor = if (!isActive) Color.Gray else tunnelState.statistics.asColor() + val leadingIconColor = if (!isActive) Color.Gray else tunnelState.statistics.asColor() - val (leadingIcon, size) = when { - tunnel.isPrimaryTunnel -> Pair(Icons.Rounded.Star, 16.dp) - tunnel.isMobileDataTunnel -> Pair(Icons.Rounded.Smartphone, 16.dp) - tunnel.isEthernetTunnel -> Pair(Icons.Rounded.SettingsEthernet, 16.dp) - else -> Pair(Icons.Rounded.Circle, 14.dp) - } + val (leadingIcon, size) = + when { + tunnel.isPrimaryTunnel -> Pair(Icons.Rounded.Star, 16.dp) + tunnel.isMobileDataTunnel -> Pair(Icons.Rounded.Smartphone, 16.dp) + tunnel.isEthernetTunnel -> Pair(Icons.Rounded.SettingsEthernet, 16.dp) + else -> Pair(Icons.Rounded.Circle, 14.dp) + } - ExpandingRowListItem( - leading = { - Icon( - leadingIcon, - stringResource(R.string.status), - tint = leadingIconColor, - modifier = Modifier.size(size), - ) - }, - text = tunnel.tunName, - onHold = { - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - onSetSelectedTunnel(tunnel) - }, - onClick = { - if (!isTv) { - if (isActive) onClick() - } else { - onSetSelectedTunnel(tunnel) - itemFocusRequester.requestFocus() - } - }, - isExpanded = expanded && isActive, - expanded = { if (isActive && expanded) TunnelStatisticsRow(tunnelState.statistics, tunnel) }, - trailing = { - if (isSelected && !isTv) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly, - ) { - IconButton( - modifier = Modifier.weight(1f), - onClick = { - onSetSelectedTunnel(null) - navController.navigate(Route.TunnelOptions(tunnel.id)) - }, - ) { - Icon( - Icons.Rounded.Settings, - stringResource(id = R.string.settings), - modifier = Modifier.size(24.dp), - ) - } - IconButton( - modifier = Modifier.weight(1f), - onClick = { - onCopy() - onSetSelectedTunnel(null) - }, - ) { - Icon( - Icons.Rounded.CopyAll, - stringResource(R.string.copy), - modifier = Modifier.size(24.dp), - ) - } - IconButton( - modifier = Modifier.weight(1f), - enabled = !isActive, - onClick = { onDelete() }, - ) { - Icon( - Icons.Rounded.Delete, - stringResource(R.string.delete_tunnel), - modifier = Modifier.size(24.dp), - ) - } - } - } else if (isTv) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly, - ) { - IconButton( - modifier = Modifier.weight(1f), - onClick = { - navController.navigate(Route.TunnelOptions(tunnel.id)) - onSetSelectedTunnel(null) - }, - ) { - Icon( - Icons.Rounded.Settings, - stringResource(id = R.string.settings), - modifier = Modifier.size(24.dp), - ) - } - IconButton( - modifier = Modifier.weight(1f), - onClick = { - if (isActive) { - onClick() - } else { - viewModel.handleEvent( - AppEvent.ShowMessage(StringValue.StringResource(R.string.turn_on_tunnel)), - ) - } - }, - ) { - Icon( - Icons.Rounded.Info, - stringResource(R.string.info), - modifier = Modifier.size(24.dp), - ) - } - IconButton( - modifier = Modifier.weight(1f), - onClick = onCopy, - ) { - Icon( - Icons.Rounded.CopyAll, - stringResource(R.string.copy), - modifier = Modifier.size(24.dp), - ) - } - IconButton( - modifier = Modifier.weight(1f), - onClick = { - if (isActive) { - viewModel.handleEvent( - AppEvent.ShowMessage(StringValue.StringResource(R.string.turn_off_tunnel)), - ) - } else { - onDelete() - } - }, - ) { - Icon( - Icons.Rounded.Delete, - stringResource(R.string.delete_tunnel), - modifier = Modifier.size(24.dp), - ) - } - ScaledSwitch( - modifier = Modifier - .focusRequester(itemFocusRequester) - .weight(1f), - checked = isActive, - onClick = onSwitchClick, - ) - } - } else { - ScaledSwitch( - modifier = Modifier.focusRequester(itemFocusRequester), - checked = isActive, - onClick = onSwitchClick, - ) - } - }, - ) + ExpandingRowListItem( + leading = { + Icon( + leadingIcon, + stringResource(R.string.status), + tint = leadingIconColor, + modifier = Modifier.size(size), + ) + }, + text = tunnel.tunName, + onHold = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + onSetSelectedTunnel(tunnel) + }, + onClick = { + if (!isTv) { + if (isActive) onClick() + } else { + onSetSelectedTunnel(tunnel) + itemFocusRequester.requestFocus() + } + }, + isExpanded = expanded && isActive, + expanded = { + if (isActive && expanded) TunnelStatisticsRow(tunnelState.statistics, tunnel) + }, + trailing = { + if (isSelected && !isTv) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + IconButton( + modifier = Modifier.weight(1f), + onClick = { + onSetSelectedTunnel(null) + navController.navigate(Route.TunnelOptions(tunnel.id)) + }, + ) { + Icon( + Icons.Rounded.Settings, + stringResource(id = R.string.settings), + modifier = Modifier.size(24.dp), + ) + } + IconButton( + modifier = Modifier.weight(1f), + onClick = { + onCopy() + onSetSelectedTunnel(null) + }, + ) { + Icon( + Icons.Rounded.CopyAll, + stringResource(R.string.copy), + modifier = Modifier.size(24.dp), + ) + } + IconButton( + modifier = Modifier.weight(1f), + enabled = !isActive, + onClick = { onDelete() }, + ) { + Icon( + Icons.Rounded.Delete, + stringResource(R.string.delete_tunnel), + modifier = Modifier.size(24.dp), + ) + } + } + } else if (isTv) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + IconButton( + modifier = Modifier.weight(1f), + onClick = { + navController.navigate(Route.TunnelOptions(tunnel.id)) + onSetSelectedTunnel(null) + }, + ) { + Icon( + Icons.Rounded.Settings, + stringResource(id = R.string.settings), + modifier = Modifier.size(24.dp), + ) + } + IconButton( + modifier = Modifier.weight(1f), + onClick = { + if (isActive) { + onClick() + } else { + viewModel.handleEvent( + AppEvent.ShowMessage( + StringValue.StringResource(R.string.turn_on_tunnel) + ) + ) + } + }, + ) { + Icon( + Icons.Rounded.Info, + stringResource(R.string.info), + modifier = Modifier.size(24.dp), + ) + } + IconButton(modifier = Modifier.weight(1f), onClick = onCopy) { + Icon( + Icons.Rounded.CopyAll, + stringResource(R.string.copy), + modifier = Modifier.size(24.dp), + ) + } + IconButton( + modifier = Modifier.weight(1f), + onClick = { + if (isActive) { + viewModel.handleEvent( + AppEvent.ShowMessage( + StringValue.StringResource(R.string.turn_off_tunnel) + ) + ) + } else { + onDelete() + } + }, + ) { + Icon( + Icons.Rounded.Delete, + stringResource(R.string.delete_tunnel), + modifier = Modifier.size(24.dp), + ) + } + ScaledSwitch( + modifier = Modifier.focusRequester(itemFocusRequester).weight(1f), + checked = isActive, + onClick = onSwitchClick, + ) + } + } else { + ScaledSwitch( + modifier = Modifier.focusRequester(itemFocusRequester), + checked = isActive, + onClick = onSwitchClick, + ) + } + }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelStatisticsRow.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelStatisticsRow.kt index d7848df2..c4a24354 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelStatisticsRow.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelStatisticsRow.kt @@ -20,48 +20,50 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.toThreeDecimalPlaceSt @Composable fun TunnelStatisticsRow(statistics: TunnelStatistics?, tunnelConf: TunnelConf) { - val config = TunnelConf.configFromAmQuick(tunnelConf.wgQuick) - config.peers.forEach { peer -> - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(end = 10.dp, bottom = 10.dp, start = 45.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start), - ) { - val peerId = peer.publicKey.toBase64().subSequence(0, 3).toString() + "***" - val peerRx = statistics?.peerStats(peer.publicKey)?.rxBytes ?: 0 - val peerTx = statistics?.peerStats(peer.publicKey)?.txBytes ?: 0 - val peerTxMB = NumberUtils.bytesToMB(peerTx).toThreeDecimalPlaceString() - val peerRxMB = NumberUtils.bytesToMB(peerRx).toThreeDecimalPlaceString() - val handshake = statistics?.peerStats(peer.publicKey)?.latestHandshakeEpochMillis?.let { - if (it == 0L) { - stringResource(R.string.never) - } else { - "${NumberUtils.getSecondsBetweenTimestampAndNow(it)} ${stringResource(R.string.sec)}" - } - } ?: stringResource(R.string.never) - Column( - verticalArrangement = Arrangement.spacedBy(10.dp), - ) { - Text( - stringResource(R.string.peer).lowercase() + ": $peerId", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.outline, - ) - Text("tx: $peerTxMB MB", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline) - } - Column( - verticalArrangement = Arrangement.spacedBy(10.dp), - ) { - Text( - stringResource(R.string.handshake) + ": $handshake", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.outline, - ) - Text("rx: $peerRxMB MB", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline) - } - } - } + val config = TunnelConf.configFromAmQuick(tunnelConf.wgQuick) + config.peers.forEach { peer -> + Row( + modifier = Modifier.fillMaxWidth().padding(end = 10.dp, bottom = 10.dp, start = 45.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start), + ) { + val peerId = peer.publicKey.toBase64().subSequence(0, 3).toString() + "***" + val peerRx = statistics?.peerStats(peer.publicKey)?.rxBytes ?: 0 + val peerTx = statistics?.peerStats(peer.publicKey)?.txBytes ?: 0 + val peerTxMB = NumberUtils.bytesToMB(peerTx).toThreeDecimalPlaceString() + val peerRxMB = NumberUtils.bytesToMB(peerRx).toThreeDecimalPlaceString() + val handshake = + statistics?.peerStats(peer.publicKey)?.latestHandshakeEpochMillis?.let { + if (it == 0L) { + stringResource(R.string.never) + } else { + "${NumberUtils.getSecondsBetweenTimestampAndNow(it)} ${stringResource(R.string.sec)}" + } + } ?: stringResource(R.string.never) + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + Text( + stringResource(R.string.peer).lowercase() + ": $peerId", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.outline, + ) + Text( + "tx: $peerTxMB MB", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.outline, + ) + } + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + Text( + stringResource(R.string.handshake) + ": $handshake", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.outline, + ) + Text( + "rx: $peerRxMB MB", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.outline, + ) + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/UrlImportDialog.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/UrlImportDialog.kt index 23f7c436..bfeddd37 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/UrlImportDialog.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/UrlImportDialog.kt @@ -19,37 +19,28 @@ import com.zaneschepke.wireguardautotunnel.R @Composable fun UrlImportDialog(onDismiss: () -> Unit, onConfirm: (String) -> Unit) { - var url by remember { mutableStateOf("") } + var url by remember { mutableStateOf("") } - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.add_from_url)) }, - text = { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), - ) { - OutlinedTextField( - value = url, - onValueChange = { url = it }, - label = { Text(stringResource(R.string.enter_config_url)) }, - modifier = Modifier.fillMaxWidth(), - ) - } - }, - confirmButton = { - TextButton( - onClick = { onConfirm(url) }, - enabled = url.isNotBlank(), - ) { - Text(stringResource(R.string.okay)) - } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text(stringResource(R.string.cancel)) - } - }, - ) + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.add_from_url)) }, + text = { + Column(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)) { + OutlinedTextField( + value = url, + onValueChange = { url = it }, + label = { Text(stringResource(R.string.enter_config_url)) }, + modifier = Modifier.fillMaxWidth(), + ) + } + }, + confirmButton = { + TextButton(onClick = { onConfirm(url) }, enabled = url.isNotBlank()) { + Text(stringResource(R.string.okay)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) } + }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/ConfigScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/ConfigScreen.kt index 64fb861e..ee9a9f74 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/ConfigScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/ConfigScreen.kt @@ -30,79 +30,89 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable -fun ConfigScreen(tunnelConf: TunnelConf?, appViewModel: AppViewModel, viewModel: ConfigViewModel = hiltViewModel()) { - val context = LocalContext.current - val keyboardController = LocalSoftwareKeyboardController.current +fun ConfigScreen( + tunnelConf: TunnelConf?, + appViewModel: AppViewModel, + viewModel: ConfigViewModel = hiltViewModel(), +) { + val context = LocalContext.current + val keyboardController = LocalSoftwareKeyboardController.current - val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val activity = context as? MainActivity + val activity = context as? MainActivity - // Secure screen due to sensitive information - DisposableEffect(Unit) { - activity?.window?.setFlags( - WindowManager.LayoutParams.FLAG_SECURE, - WindowManager.LayoutParams.FLAG_SECURE, - ) - onDispose { - activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) - } - } + // Secure screen due to sensitive information + DisposableEffect(Unit) { + activity + ?.window + ?.setFlags( + WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.FLAG_SECURE, + ) + onDispose { activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) } + } - LaunchedEffect(Unit) { - // set callback for navbar to invoke save - appViewModel.handleEvent( - AppEvent.SetScreenAction { - keyboardController?.hide() - viewModel.save(tunnelConf) - }, - ) - } + LaunchedEffect(Unit) { + // set callback for navbar to invoke save + appViewModel.handleEvent( + AppEvent.SetScreenAction { + keyboardController?.hide() + viewModel.save(tunnelConf) + } + ) + } - LaunchedEffect(tunnelConf) { - viewModel.initFromTunnel(tunnelConf) - } + LaunchedEffect(tunnelConf) { viewModel.initFromTunnel(tunnelConf) } - LaunchedEffect(uiState.success) { - if (uiState.success == true) { - appViewModel.handleEvent(AppEvent.ShowMessage(StringValue.StringResource(R.string.config_changes_saved))) - appViewModel.handleEvent(AppEvent.PopBackStack(true)) - } - } + LaunchedEffect(uiState.success) { + if (uiState.success == true) { + appViewModel.handleEvent( + AppEvent.ShowMessage(StringValue.StringResource(R.string.config_changes_saved)) + ) + appViewModel.handleEvent(AppEvent.PopBackStack(true)) + } + } - LaunchedEffect(uiState.message) { - uiState.message?.let { message -> - appViewModel.handleEvent(AppEvent.ShowMessage(message)) - viewModel.setMessage(null) - } - } + LaunchedEffect(uiState.message) { + uiState.message?.let { message -> + appViewModel.handleEvent(AppEvent.ShowMessage(message)) + viewModel.setMessage(null) + } + } - if (uiState.showAuthPrompt) { - AuthorizationPrompt( - onSuccess = { - viewModel.toggleShowAuthPrompt() - viewModel.onAuthenticated() - }, - onError = { - viewModel.toggleShowAuthPrompt() - appViewModel.handleEvent(AppEvent.ShowMessage(StringValue.StringResource(R.string.error_authentication_failed))) - }, - onFailure = { - viewModel.toggleShowAuthPrompt() - appViewModel.handleEvent(AppEvent.ShowMessage(StringValue.StringResource(R.string.error_authorization_failed))) - }, - ) - } + if (uiState.showAuthPrompt) { + AuthorizationPrompt( + onSuccess = { + viewModel.toggleShowAuthPrompt() + viewModel.onAuthenticated() + }, + onError = { + viewModel.toggleShowAuthPrompt() + appViewModel.handleEvent( + AppEvent.ShowMessage( + StringValue.StringResource(R.string.error_authentication_failed) + ) + ) + }, + onFailure = { + viewModel.toggleShowAuthPrompt() + appViewModel.handleEvent( + AppEvent.ShowMessage( + StringValue.StringResource(R.string.error_authorization_failed) + ) + ) + }, + ) + } - Column( - verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(horizontal = 24.dp), - ) { - InterfaceSection(uiState, viewModel) - PeersSection(uiState, viewModel) - AddPeerButton(viewModel) - } + Column( + verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), + modifier = + Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 24.dp), + ) { + InterfaceSection(uiState, viewModel) + PeersSection(uiState, viewModel) + AddPeerButton(viewModel) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/ConfigViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/ConfigViewModel.kt index 4aa8f294..21f429e8 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/ConfigViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/ConfigViewModel.kt @@ -12,162 +12,155 @@ import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy import com.zaneschepke.wireguardautotunnel.ui.state.PeerProxy import com.zaneschepke.wireguardautotunnel.util.StringValue import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import javax.inject.Inject @HiltViewModel -class ConfigViewModel @Inject constructor( - private val tunnelRepository: TunnelRepository, - @IoDispatcher private val ioDispatcher: CoroutineDispatcher, +class ConfigViewModel +@Inject +constructor( + private val tunnelRepository: TunnelRepository, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, ) : ViewModel() { - private val _uiState = MutableStateFlow(ConfigUiState()) - val uiState: StateFlow = _uiState.asStateFlow() + private val _uiState = MutableStateFlow(ConfigUiState()) + val uiState: StateFlow = _uiState.asStateFlow() - fun initFromTunnel(tunnelConf: TunnelConf?) { - if (tunnelConf == null) return - _uiState.update { - val proxy = ConfigProxy.from(tunnelConf.toAmConfig()) - it.copy( - tunnelName = tunnelConf.name, - configProxy = proxy, - showScripts = proxy.hasScripts(), - showAmneziaValues = proxy.`interface`.junkPacketCount.isNotBlank(), - isAuthenticated = false, - ) - } - } + fun initFromTunnel(tunnelConf: TunnelConf?) { + if (tunnelConf == null) return + _uiState.update { + val proxy = ConfigProxy.from(tunnelConf.toAmConfig()) + it.copy( + tunnelName = tunnelConf.name, + configProxy = proxy, + showScripts = proxy.hasScripts(), + showAmneziaValues = proxy.`interface`.junkPacketCount.isNotBlank(), + isAuthenticated = false, + ) + } + } - fun updateTunnelName(name: String) { - _uiState.update { - it.copy(tunnelName = name) - } - } + fun updateTunnelName(name: String) { + _uiState.update { it.copy(tunnelName = name) } + } - fun updateInterface(newInterface: InterfaceProxy) { - _uiState.update { - it.copy( - configProxy = it.configProxy.copy( - `interface` = newInterface, - ), - ) - } - } + fun updateInterface(newInterface: InterfaceProxy) { + _uiState.update { it.copy(configProxy = it.configProxy.copy(`interface` = newInterface)) } + } - fun toggleAmneziaValues() { - _uiState.update { - it.copy(showAmneziaValues = !it.showAmneziaValues) - } - } + fun toggleAmneziaValues() { + _uiState.update { it.copy(showAmneziaValues = !it.showAmneziaValues) } + } - fun toggleScripts() { - _uiState.update { - it.copy(showScripts = !it.showScripts) - } - } + fun toggleScripts() { + _uiState.update { it.copy(showScripts = !it.showScripts) } + } - fun toggleAmneziaCompatibility() { - val (show, `interface`) = with(_uiState.value.configProxy) { - if (`interface`.isAmneziaCompatibilityModeSet()) { - Pair(false, `interface`.resetAmneziaProperties()) - } else { - Pair(true, `interface`.toAmneziaCompatibilityConfig()) - } - } - _uiState.update { - it.copy( - showAmneziaValues = show, - configProxy = it.configProxy.copy( - `interface` = `interface`, - ), - ) - } - } + fun toggleAmneziaCompatibility() { + val (show, `interface`) = + with(_uiState.value.configProxy) { + if (`interface`.isAmneziaCompatibilityModeSet()) { + Pair(false, `interface`.resetAmneziaProperties()) + } else { + Pair(true, `interface`.toAmneziaCompatibilityConfig()) + } + } + _uiState.update { + it.copy( + showAmneziaValues = show, + configProxy = it.configProxy.copy(`interface` = `interface`), + ) + } + } - fun addPeer() { - _uiState.update { currentState -> - currentState.copy( - configProxy = currentState.configProxy.copy( - peers = currentState.configProxy.peers + PeerProxy(), - ), - ) - } - } + fun addPeer() { + _uiState.update { currentState -> + currentState.copy( + configProxy = + currentState.configProxy.copy( + peers = currentState.configProxy.peers + PeerProxy() + ) + ) + } + } - fun removePeer(index: Int) { - _uiState.update { currentState -> - currentState.copy( - configProxy = currentState.configProxy.copy( - peers = currentState.configProxy.peers.toMutableList().apply { removeAt(index) }, - ), - ) - } - } + fun removePeer(index: Int) { + _uiState.update { currentState -> + currentState.copy( + configProxy = + currentState.configProxy.copy( + peers = + currentState.configProxy.peers.toMutableList().apply { removeAt(index) } + ) + ) + } + } - fun updatePeer(index: Int, peer: PeerProxy) { - _uiState.update { currentState -> - currentState.copy( - configProxy = currentState.configProxy.copy( - peers = currentState.configProxy.peers.toMutableList().apply { set(index, peer) }, - ), - ) - } - } + fun updatePeer(index: Int, peer: PeerProxy) { + _uiState.update { currentState -> + currentState.copy( + configProxy = + currentState.configProxy.copy( + peers = + currentState.configProxy.peers.toMutableList().apply { + set(index, peer) + } + ) + ) + } + } - fun toggleLanExclusion(index: Int) { - val peer = _uiState.value.configProxy.peers[index] - val updated = if (peer.isLanExcluded()) peer.includeLan() else peer.excludeLan() - updatePeer(index, updated) - } + fun toggleLanExclusion(index: Int) { + val peer = _uiState.value.configProxy.peers[index] + val updated = if (peer.isLanExcluded()) peer.includeLan() else peer.excludeLan() + updatePeer(index, updated) + } - fun setMessage(message: StringValue?) { - _uiState.update { - it.copy(message = message) - } - } + fun setMessage(message: StringValue?) { + _uiState.update { it.copy(message = message) } + } - // TODO improve error messaging - fun save(tunnelConf: TunnelConf?) = viewModelScope.launch(ioDispatcher) { - val message = try { - val saveConfig = buildTunnelConfFromState(tunnelConf) - tunnelRepository.save(saveConfig) - _uiState.update { it.copy(success = true) } - } catch (e: Exception) { - setMessage( - e.message?.let { message -> - (StringValue.DynamicString(message)) - } ?: StringValue.StringResource(R.string.unknown_error), - ) - } - } + // TODO improve error messaging + fun save(tunnelConf: TunnelConf?) = + viewModelScope.launch(ioDispatcher) { + val message = + try { + val saveConfig = buildTunnelConfFromState(tunnelConf) + tunnelRepository.save(saveConfig) + _uiState.update { it.copy(success = true) } + } catch (e: Exception) { + setMessage( + e.message?.let { message -> (StringValue.DynamicString(message)) } + ?: StringValue.StringResource(R.string.unknown_error) + ) + } + } - private fun buildTunnelConfFromState(tunnelConf: TunnelConf?): TunnelConf { - val (wg, am) = _uiState.value.configProxy.buildConfigs() - val name = _uiState.value.tunnelName - return tunnelConf?.copyWithCallback( - tunName = name, - amQuick = am.toAwgQuickString(true), - wgQuick = wg.toWgQuickString(true), - ) ?: TunnelConf( - tunName = name, amQuick = am.toAwgQuickString(true), - wgQuick = wg.toWgQuickString(true), - ) - } + private fun buildTunnelConfFromState(tunnelConf: TunnelConf?): TunnelConf { + val (wg, am) = _uiState.value.configProxy.buildConfigs() + val name = _uiState.value.tunnelName + return tunnelConf?.copyWithCallback( + tunName = name, + amQuick = am.toAwgQuickString(true), + wgQuick = wg.toWgQuickString(true), + ) + ?: TunnelConf( + tunName = name, + amQuick = am.toAwgQuickString(true), + wgQuick = wg.toWgQuickString(true), + ) + } - fun onAuthenticated() { - _uiState.update { - it.copy(isAuthenticated = true) - } - } + fun onAuthenticated() { + _uiState.update { it.copy(isAuthenticated = true) } + } - fun toggleShowAuthPrompt() { - _uiState.update { - it.copy(showAuthPrompt = !it.showAuthPrompt) - } - } + fun toggleShowAuthPrompt() { + _uiState.update { it.copy(showAuthPrompt = !it.showAuthPrompt) } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/components/AddPeerButton.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/components/AddPeerButton.kt index b0b01413..97611c0c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/components/AddPeerButton.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/components/AddPeerButton.kt @@ -16,13 +16,11 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.ConfigViewMode @Composable fun AddPeerButton(viewModel: ConfigViewModel) { - Row( - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth().padding(bottom = 24.dp), - ) { - TextButton(onClick = { viewModel.addPeer() }) { - Text(stringResource(R.string.add_peer)) - } - } + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().padding(bottom = 24.dp), + ) { + TextButton(onClick = { viewModel.addPeer() }) { Text(stringResource(R.string.add_peer)) } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/components/InterfaceDropdown.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/components/InterfaceDropdown.kt index 6398b33a..5fe3af5c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/components/InterfaceDropdown.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/components/InterfaceDropdown.kt @@ -21,52 +21,61 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize @Composable fun InterfaceDropdown( - expanded: Boolean, - onExpandedChange: (Boolean) -> Unit, - showScripts: Boolean, - showAmneziaValues: Boolean, - isAmneziaCompatibilitySet: Boolean, - onToggleScripts: () -> Unit, - onToggleAmneziaValues: () -> Unit, - onToggleAmneziaCompatibility: () -> Unit, + expanded: Boolean, + onExpandedChange: (Boolean) -> Unit, + showScripts: Boolean, + showAmneziaValues: Boolean, + isAmneziaCompatibilitySet: Boolean, + onToggleScripts: () -> Unit, + onToggleAmneziaValues: () -> Unit, + onToggleAmneziaCompatibility: () -> Unit, ) { - Column { - IconButton( - modifier = Modifier.size(iconSize), - onClick = { onExpandedChange(true) }, - ) { - Icon(Icons.Rounded.MoreVert, contentDescription = "More") - } - DropdownMenu( - expanded = expanded, - onDismissRequest = { onExpandedChange(false) }, - modifier = Modifier.shadow(12.dp).background(MaterialTheme.colorScheme.surface), - ) { - DropdownMenuItem( - text = { Text(if (showScripts) stringResource(R.string.hide_scripts) else stringResource(R.string.show_scripts)) }, - onClick = { - onToggleScripts() - onExpandedChange(false) - }, - ) - DropdownMenuItem( - text = { Text(if (showAmneziaValues) stringResource(R.string.hide_amnezia_properties) else stringResource(R.string.show_amnezia_properties)) }, - onClick = { - onToggleAmneziaValues() - onExpandedChange(false) - }, - ) - DropdownMenuItem( - text = { - Text( - if (isAmneziaCompatibilitySet) stringResource(R.string.remove_amnezia_compatibility) else stringResource(R.string.enable_amnezia_compatibility), - ) - }, - onClick = { - onToggleAmneziaCompatibility() - onExpandedChange(false) - }, - ) - } - } + Column { + IconButton(modifier = Modifier.size(iconSize), onClick = { onExpandedChange(true) }) { + Icon(Icons.Rounded.MoreVert, contentDescription = "More") + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { onExpandedChange(false) }, + modifier = Modifier.shadow(12.dp).background(MaterialTheme.colorScheme.surface), + ) { + DropdownMenuItem( + text = { + Text( + if (showScripts) stringResource(R.string.hide_scripts) + else stringResource(R.string.show_scripts) + ) + }, + onClick = { + onToggleScripts() + onExpandedChange(false) + }, + ) + DropdownMenuItem( + text = { + Text( + if (showAmneziaValues) stringResource(R.string.hide_amnezia_properties) + else stringResource(R.string.show_amnezia_properties) + ) + }, + onClick = { + onToggleAmneziaValues() + onExpandedChange(false) + }, + ) + DropdownMenuItem( + text = { + Text( + if (isAmneziaCompatibilitySet) + stringResource(R.string.remove_amnezia_compatibility) + else stringResource(R.string.enable_amnezia_compatibility) + ) + }, + onClick = { + onToggleAmneziaCompatibility() + onExpandedChange(false) + }, + ) + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/components/InterfaceFields.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/components/InterfaceFields.kt index 474a9028..e86d1f42 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/components/InterfaceFields.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/components/InterfaceFields.kt @@ -30,191 +30,202 @@ import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy @Composable fun InterfaceFields( - interfaceState: InterfaceProxy, - showAuthPrompt: () -> Unit, - isAuthenticated: Boolean, - showScripts: Boolean, - showAmneziaValues: Boolean, - onInterfaceChange: (InterfaceProxy) -> Unit, + interfaceState: InterfaceProxy, + showAuthPrompt: () -> Unit, + isAuthenticated: Boolean, + showScripts: Boolean, + showAmneziaValues: Boolean, + onInterfaceChange: (InterfaceProxy) -> Unit, ) { - val keyboardController = LocalSoftwareKeyboardController.current - val clipboardManager = LocalClipboardManager.current - val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }) - val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done) + val keyboardController = LocalSoftwareKeyboardController.current + val clipboardManager = LocalClipboardManager.current + val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }) + val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done) - OutlinedTextField( - value = interfaceState.privateKey, - onValueChange = { onInterfaceChange(interfaceState.copy(privateKey = it)) }, - label = { Text(stringResource(R.string.private_key), style = MaterialTheme.typography.bodyMedium) }, - modifier = Modifier.fillMaxWidth().clickable { - if (!isAuthenticated) showAuthPrompt() - }, - visualTransformation = if (isAuthenticated) VisualTransformation.None else PasswordVisualTransformation(), - trailingIcon = { - IconButton(enabled = isAuthenticated, onClick = { - val keypair = com.wireguard.crypto.KeyPair() - onInterfaceChange( - interfaceState.copy( - privateKey = keypair.privateKey.toBase64(), - publicKey = keypair.publicKey.toBase64(), - ), - ) - }) { - Icon( - Icons.Rounded.Refresh, - stringResource(R.string.rotate_keys), - tint = if (isAuthenticated) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.outline, - ) - } - }, - enabled = isAuthenticated, - singleLine = true, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - ) - OutlinedTextField( - value = interfaceState.publicKey, - onValueChange = { onInterfaceChange(interfaceState.copy(publicKey = it)) }, - label = { Text(stringResource(R.string.public_key), style = MaterialTheme.typography.bodyMedium) }, - enabled = false, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - trailingIcon = { - IconButton(onClick = { - clipboardManager.setText(AnnotatedString(interfaceState.publicKey)) - }) { - Icon(Icons.Rounded.ContentCopy, stringResource(R.string.copy_public_key)) - } - }, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - ) - ConfigurationTextBox( - value = interfaceState.addresses, - onValueChange = { onInterfaceChange(interfaceState.copy(addresses = it)) }, - label = stringResource(R.string.addresses), - hint = stringResource(R.string.comma_separated_list), - modifier = Modifier.fillMaxWidth(), - ) - ConfigurationTextBox( - value = interfaceState.listenPort, - onValueChange = { onInterfaceChange(interfaceState.copy(listenPort = it)) }, - label = stringResource(R.string.listen_port), - hint = stringResource(R.string.random), - modifier = Modifier.fillMaxWidth(), - ) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(6.dp), - ) { - ConfigurationTextBox( - value = interfaceState.dnsServers, - onValueChange = { onInterfaceChange(interfaceState.copy(dnsServers = it)) }, - label = stringResource(R.string.dns_servers), - hint = stringResource(R.string.comma_separated_list), - modifier = Modifier.weight(3f), - ) - ConfigurationTextBox( - value = interfaceState.mtu, - onValueChange = { onInterfaceChange(interfaceState.copy(mtu = it)) }, - label = stringResource(R.string.mtu), - hint = stringResource(R.string.auto), - modifier = Modifier.weight(2f), - ) - } - if (showScripts) { - ConfigurationTextBox( - value = interfaceState.preUp, - onValueChange = { onInterfaceChange(interfaceState.copy(preUp = it)) }, - label = stringResource(R.string.pre_up), - hint = stringResource(R.string.comma_separated_list).lowercase(), - modifier = Modifier.fillMaxWidth(), - ) - ConfigurationTextBox( - value = interfaceState.postUp, - onValueChange = { onInterfaceChange(interfaceState.copy(postUp = it)) }, - label = stringResource(R.string.post_up), - hint = stringResource(R.string.comma_separated_list).lowercase(), - modifier = Modifier.fillMaxWidth(), - ) - ConfigurationTextBox( - value = interfaceState.preDown, - onValueChange = { onInterfaceChange(interfaceState.copy(preDown = it)) }, - label = stringResource(R.string.pre_down), - hint = stringResource(R.string.comma_separated_list).lowercase(), - modifier = Modifier.fillMaxWidth(), - ) - ConfigurationTextBox( - value = interfaceState.postDown, - onValueChange = { onInterfaceChange(interfaceState.copy(postDown = it)) }, - label = stringResource(R.string.post_down), - hint = stringResource(R.string.comma_separated_list).lowercase(), - modifier = Modifier.fillMaxWidth(), - ) - } - if (showAmneziaValues) { - ConfigurationTextBox( - value = interfaceState.junkPacketCount, - onValueChange = { onInterfaceChange(interfaceState.copy(junkPacketCount = it)) }, - label = stringResource(R.string.junk_packet_count), - hint = stringResource(R.string.junk_packet_count).lowercase(), - modifier = Modifier.fillMaxWidth(), - ) - ConfigurationTextBox( - value = interfaceState.junkPacketMinSize, - onValueChange = { onInterfaceChange(interfaceState.copy(junkPacketMinSize = it)) }, - label = stringResource(R.string.junk_packet_minimum_size), - hint = stringResource(R.string.junk_packet_minimum_size).lowercase(), - modifier = Modifier.fillMaxWidth(), - ) - ConfigurationTextBox( - value = interfaceState.junkPacketMaxSize, - onValueChange = { onInterfaceChange(interfaceState.copy(junkPacketMaxSize = it)) }, - label = stringResource(R.string.junk_packet_maximum_size), - hint = stringResource(R.string.junk_packet_maximum_size).lowercase(), - modifier = Modifier.fillMaxWidth(), - ) - ConfigurationTextBox( - value = interfaceState.initPacketJunkSize, - onValueChange = { onInterfaceChange(interfaceState.copy(initPacketJunkSize = it)) }, - label = stringResource(R.string.init_packet_junk_size), - hint = stringResource(R.string.init_packet_junk_size).lowercase(), - modifier = Modifier.fillMaxWidth(), - ) - ConfigurationTextBox( - value = interfaceState.responsePacketJunkSize, - onValueChange = { onInterfaceChange(interfaceState.copy(responsePacketJunkSize = it)) }, - label = stringResource(R.string.response_packet_junk_size), - hint = stringResource(R.string.response_packet_junk_size).lowercase(), - modifier = Modifier.fillMaxWidth(), - ) - ConfigurationTextBox( - value = interfaceState.initPacketMagicHeader, - onValueChange = { onInterfaceChange(interfaceState.copy(initPacketMagicHeader = it)) }, - label = stringResource(R.string.init_packet_magic_header), - hint = stringResource(R.string.init_packet_magic_header).lowercase(), - modifier = Modifier.fillMaxWidth(), - ) - ConfigurationTextBox( - value = interfaceState.responsePacketMagicHeader, - onValueChange = { onInterfaceChange(interfaceState.copy(responsePacketMagicHeader = it)) }, - label = stringResource(R.string.response_packet_magic_header), - hint = stringResource(R.string.response_packet_magic_header).lowercase(), - modifier = Modifier.fillMaxWidth(), - ) - ConfigurationTextBox( - value = interfaceState.underloadPacketMagicHeader, - onValueChange = { onInterfaceChange(interfaceState.copy(underloadPacketMagicHeader = it)) }, - label = stringResource(R.string.underload_packet_magic_header), - hint = stringResource(R.string.underload_packet_magic_header).lowercase(), - modifier = Modifier.fillMaxWidth(), - ) - ConfigurationTextBox( - value = interfaceState.transportPacketMagicHeader, - onValueChange = { onInterfaceChange(interfaceState.copy(transportPacketMagicHeader = it)) }, - label = stringResource(R.string.transport_packet_magic_header), - hint = stringResource(R.string.transport_packet_magic_header).lowercase(), - modifier = Modifier.fillMaxWidth(), - ) - } + OutlinedTextField( + value = interfaceState.privateKey, + onValueChange = { onInterfaceChange(interfaceState.copy(privateKey = it)) }, + label = { + Text(stringResource(R.string.private_key), style = MaterialTheme.typography.bodyMedium) + }, + modifier = Modifier.fillMaxWidth().clickable { if (!isAuthenticated) showAuthPrompt() }, + visualTransformation = + if (isAuthenticated) VisualTransformation.None else PasswordVisualTransformation(), + trailingIcon = { + IconButton( + enabled = isAuthenticated, + onClick = { + val keypair = com.wireguard.crypto.KeyPair() + onInterfaceChange( + interfaceState.copy( + privateKey = keypair.privateKey.toBase64(), + publicKey = keypair.publicKey.toBase64(), + ) + ) + }, + ) { + Icon( + Icons.Rounded.Refresh, + stringResource(R.string.rotate_keys), + tint = + if (isAuthenticated) MaterialTheme.colorScheme.onSurface + else MaterialTheme.colorScheme.outline, + ) + } + }, + enabled = isAuthenticated, + singleLine = true, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + ) + OutlinedTextField( + value = interfaceState.publicKey, + onValueChange = { onInterfaceChange(interfaceState.copy(publicKey = it)) }, + label = { + Text(stringResource(R.string.public_key), style = MaterialTheme.typography.bodyMedium) + }, + enabled = false, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + trailingIcon = { + IconButton( + onClick = { clipboardManager.setText(AnnotatedString(interfaceState.publicKey)) } + ) { + Icon(Icons.Rounded.ContentCopy, stringResource(R.string.copy_public_key)) + } + }, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + ) + ConfigurationTextBox( + value = interfaceState.addresses, + onValueChange = { onInterfaceChange(interfaceState.copy(addresses = it)) }, + label = stringResource(R.string.addresses), + hint = stringResource(R.string.comma_separated_list), + modifier = Modifier.fillMaxWidth(), + ) + ConfigurationTextBox( + value = interfaceState.listenPort, + onValueChange = { onInterfaceChange(interfaceState.copy(listenPort = it)) }, + label = stringResource(R.string.listen_port), + hint = stringResource(R.string.random), + modifier = Modifier.fillMaxWidth(), + ) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(6.dp)) { + ConfigurationTextBox( + value = interfaceState.dnsServers, + onValueChange = { onInterfaceChange(interfaceState.copy(dnsServers = it)) }, + label = stringResource(R.string.dns_servers), + hint = stringResource(R.string.comma_separated_list), + modifier = Modifier.weight(3f), + ) + ConfigurationTextBox( + value = interfaceState.mtu, + onValueChange = { onInterfaceChange(interfaceState.copy(mtu = it)) }, + label = stringResource(R.string.mtu), + hint = stringResource(R.string.auto), + modifier = Modifier.weight(2f), + ) + } + if (showScripts) { + ConfigurationTextBox( + value = interfaceState.preUp, + onValueChange = { onInterfaceChange(interfaceState.copy(preUp = it)) }, + label = stringResource(R.string.pre_up), + hint = stringResource(R.string.comma_separated_list).lowercase(), + modifier = Modifier.fillMaxWidth(), + ) + ConfigurationTextBox( + value = interfaceState.postUp, + onValueChange = { onInterfaceChange(interfaceState.copy(postUp = it)) }, + label = stringResource(R.string.post_up), + hint = stringResource(R.string.comma_separated_list).lowercase(), + modifier = Modifier.fillMaxWidth(), + ) + ConfigurationTextBox( + value = interfaceState.preDown, + onValueChange = { onInterfaceChange(interfaceState.copy(preDown = it)) }, + label = stringResource(R.string.pre_down), + hint = stringResource(R.string.comma_separated_list).lowercase(), + modifier = Modifier.fillMaxWidth(), + ) + ConfigurationTextBox( + value = interfaceState.postDown, + onValueChange = { onInterfaceChange(interfaceState.copy(postDown = it)) }, + label = stringResource(R.string.post_down), + hint = stringResource(R.string.comma_separated_list).lowercase(), + modifier = Modifier.fillMaxWidth(), + ) + } + if (showAmneziaValues) { + ConfigurationTextBox( + value = interfaceState.junkPacketCount, + onValueChange = { onInterfaceChange(interfaceState.copy(junkPacketCount = it)) }, + label = stringResource(R.string.junk_packet_count), + hint = stringResource(R.string.junk_packet_count).lowercase(), + modifier = Modifier.fillMaxWidth(), + ) + ConfigurationTextBox( + value = interfaceState.junkPacketMinSize, + onValueChange = { onInterfaceChange(interfaceState.copy(junkPacketMinSize = it)) }, + label = stringResource(R.string.junk_packet_minimum_size), + hint = stringResource(R.string.junk_packet_minimum_size).lowercase(), + modifier = Modifier.fillMaxWidth(), + ) + ConfigurationTextBox( + value = interfaceState.junkPacketMaxSize, + onValueChange = { onInterfaceChange(interfaceState.copy(junkPacketMaxSize = it)) }, + label = stringResource(R.string.junk_packet_maximum_size), + hint = stringResource(R.string.junk_packet_maximum_size).lowercase(), + modifier = Modifier.fillMaxWidth(), + ) + ConfigurationTextBox( + value = interfaceState.initPacketJunkSize, + onValueChange = { onInterfaceChange(interfaceState.copy(initPacketJunkSize = it)) }, + label = stringResource(R.string.init_packet_junk_size), + hint = stringResource(R.string.init_packet_junk_size).lowercase(), + modifier = Modifier.fillMaxWidth(), + ) + ConfigurationTextBox( + value = interfaceState.responsePacketJunkSize, + onValueChange = { onInterfaceChange(interfaceState.copy(responsePacketJunkSize = it)) }, + label = stringResource(R.string.response_packet_junk_size), + hint = stringResource(R.string.response_packet_junk_size).lowercase(), + modifier = Modifier.fillMaxWidth(), + ) + ConfigurationTextBox( + value = interfaceState.initPacketMagicHeader, + onValueChange = { onInterfaceChange(interfaceState.copy(initPacketMagicHeader = it)) }, + label = stringResource(R.string.init_packet_magic_header), + hint = stringResource(R.string.init_packet_magic_header).lowercase(), + modifier = Modifier.fillMaxWidth(), + ) + ConfigurationTextBox( + value = interfaceState.responsePacketMagicHeader, + onValueChange = { + onInterfaceChange(interfaceState.copy(responsePacketMagicHeader = it)) + }, + label = stringResource(R.string.response_packet_magic_header), + hint = stringResource(R.string.response_packet_magic_header).lowercase(), + modifier = Modifier.fillMaxWidth(), + ) + ConfigurationTextBox( + value = interfaceState.underloadPacketMagicHeader, + onValueChange = { + onInterfaceChange(interfaceState.copy(underloadPacketMagicHeader = it)) + }, + label = stringResource(R.string.underload_packet_magic_header), + hint = stringResource(R.string.underload_packet_magic_header).lowercase(), + modifier = Modifier.fillMaxWidth(), + ) + ConfigurationTextBox( + value = interfaceState.transportPacketMagicHeader, + onValueChange = { + onInterfaceChange(interfaceState.copy(transportPacketMagicHeader = it)) + }, + label = stringResource(R.string.transport_packet_magic_header), + hint = stringResource(R.string.transport_packet_magic_header).lowercase(), + modifier = Modifier.fillMaxWidth(), + ) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/components/InterfaceSection.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/components/InterfaceSection.kt index d3fdaf3a..82a4c67b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/components/InterfaceSection.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/components/InterfaceSection.kt @@ -26,48 +26,46 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.state.ConfigUi @Composable fun InterfaceSection(uiState: ConfigUiState, viewModel: ConfigViewModel) { - var isDropDownExpanded by remember { mutableStateOf(false) } + var isDropDownExpanded by remember { mutableStateOf(false) } - Surface( - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surface, - ) { - Column( - verticalArrangement = Arrangement.spacedBy(6.dp), - modifier = Modifier.padding(16.dp).focusGroup(), - ) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), - ) { - GroupLabel(stringResource(R.string.interface_)) - InterfaceDropdown( - expanded = isDropDownExpanded, - onExpandedChange = { isDropDownExpanded = it }, - showScripts = uiState.showScripts, - showAmneziaValues = uiState.showAmneziaValues, - isAmneziaCompatibilitySet = uiState.configProxy.`interface`.isAmneziaCompatibilityModeSet(), - onToggleScripts = viewModel::toggleScripts, - onToggleAmneziaValues = viewModel::toggleAmneziaValues, - onToggleAmneziaCompatibility = viewModel::toggleAmneziaCompatibility, - ) - } - ConfigurationTextBox( - value = uiState.tunnelName, - onValueChange = viewModel::updateTunnelName, - label = stringResource(R.string.name), - hint = stringResource(R.string.tunnel_name).lowercase(), - modifier = Modifier.fillMaxWidth(), - ) - InterfaceFields( - interfaceState = uiState.configProxy.`interface`, - showAuthPrompt = { viewModel.toggleShowAuthPrompt() }, - showScripts = uiState.showScripts, - showAmneziaValues = uiState.showAmneziaValues, - onInterfaceChange = viewModel::updateInterface, - isAuthenticated = uiState.isAuthenticated, - ) - } - } + Surface(shape = RoundedCornerShape(12.dp), color = MaterialTheme.colorScheme.surface) { + Column( + verticalArrangement = Arrangement.spacedBy(6.dp), + modifier = Modifier.padding(16.dp).focusGroup(), + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + GroupLabel(stringResource(R.string.interface_)) + InterfaceDropdown( + expanded = isDropDownExpanded, + onExpandedChange = { isDropDownExpanded = it }, + showScripts = uiState.showScripts, + showAmneziaValues = uiState.showAmneziaValues, + isAmneziaCompatibilitySet = + uiState.configProxy.`interface`.isAmneziaCompatibilityModeSet(), + onToggleScripts = viewModel::toggleScripts, + onToggleAmneziaValues = viewModel::toggleAmneziaValues, + onToggleAmneziaCompatibility = viewModel::toggleAmneziaCompatibility, + ) + } + ConfigurationTextBox( + value = uiState.tunnelName, + onValueChange = viewModel::updateTunnelName, + label = stringResource(R.string.name), + hint = stringResource(R.string.tunnel_name).lowercase(), + modifier = Modifier.fillMaxWidth(), + ) + InterfaceFields( + interfaceState = uiState.configProxy.`interface`, + showAuthPrompt = { viewModel.toggleShowAuthPrompt() }, + showScripts = uiState.showScripts, + showAmneziaValues = uiState.showAmneziaValues, + onInterfaceChange = viewModel::updateInterface, + isAuthenticated = uiState.isAuthenticated, + ) + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/components/PeerFields.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/components/PeerFields.kt index f2343b78..92411817 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/components/PeerFields.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/components/PeerFields.kt @@ -21,54 +21,87 @@ import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox import com.zaneschepke.wireguardautotunnel.ui.state.PeerProxy @Composable -fun PeerFields(peer: PeerProxy, onPeerChange: (PeerProxy) -> Unit, showAuthPrompt: () -> Unit, isAuthenticated: Boolean) { - val keyboardController = LocalSoftwareKeyboardController.current - val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }) - val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done) +fun PeerFields( + peer: PeerProxy, + onPeerChange: (PeerProxy) -> Unit, + showAuthPrompt: () -> Unit, + isAuthenticated: Boolean, +) { + val keyboardController = LocalSoftwareKeyboardController.current + val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }) + val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done) - ConfigurationTextBox( - value = peer.publicKey, - onValueChange = { onPeerChange(peer.copy(publicKey = it)) }, - label = stringResource(R.string.public_key), - hint = stringResource(R.string.base64_key), - modifier = Modifier.fillMaxWidth(), - ) - OutlinedTextField( - visualTransformation = if (isAuthenticated) VisualTransformation.None else PasswordVisualTransformation(), - value = peer.preSharedKey, - enabled = isAuthenticated, - onValueChange = { onPeerChange(peer.copy(preSharedKey = it)) }, - label = { Text(stringResource(R.string.preshared_key), style = MaterialTheme.typography.bodyMedium) }, - placeholder = { Text(stringResource(R.string.optional), style = MaterialTheme.typography.bodyMedium) }, - modifier = Modifier.fillMaxWidth().clickable { if (!isAuthenticated) showAuthPrompt() }, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - singleLine = true, - ) - OutlinedTextField( - value = peer.persistentKeepalive, - onValueChange = { onPeerChange(peer.copy(persistentKeepalive = it)) }, - label = { Text(stringResource(R.string.persistent_keepalive), style = MaterialTheme.typography.bodyMedium) }, - placeholder = { Text(stringResource(R.string.optional), style = MaterialTheme.typography.bodyMedium) }, - trailingIcon = { Text(stringResource(R.string.seconds), style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(end = 10.dp)) }, - modifier = Modifier.fillMaxWidth(), - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - ) - ConfigurationTextBox( - value = peer.endpoint, - onValueChange = { onPeerChange(peer.copy(endpoint = it)) }, - label = stringResource(R.string.endpoint), - hint = stringResource(R.string.endpoint).lowercase(), - modifier = Modifier.fillMaxWidth(), - ) - OutlinedTextField( - value = peer.allowedIps, - onValueChange = { onPeerChange(peer.copy(allowedIps = it)) }, - label = { Text(stringResource(R.string.allowed_ips), style = MaterialTheme.typography.bodyMedium) }, - placeholder = { Text(stringResource(R.string.comma_separated_list), style = MaterialTheme.typography.bodyMedium) }, - modifier = Modifier.fillMaxWidth(), - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - ) + ConfigurationTextBox( + value = peer.publicKey, + onValueChange = { onPeerChange(peer.copy(publicKey = it)) }, + label = stringResource(R.string.public_key), + hint = stringResource(R.string.base64_key), + modifier = Modifier.fillMaxWidth(), + ) + OutlinedTextField( + visualTransformation = + if (isAuthenticated) VisualTransformation.None else PasswordVisualTransformation(), + value = peer.preSharedKey, + enabled = isAuthenticated, + onValueChange = { onPeerChange(peer.copy(preSharedKey = it)) }, + label = { + Text( + stringResource(R.string.preshared_key), + style = MaterialTheme.typography.bodyMedium, + ) + }, + placeholder = { + Text(stringResource(R.string.optional), style = MaterialTheme.typography.bodyMedium) + }, + modifier = Modifier.fillMaxWidth().clickable { if (!isAuthenticated) showAuthPrompt() }, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + singleLine = true, + ) + OutlinedTextField( + value = peer.persistentKeepalive, + onValueChange = { onPeerChange(peer.copy(persistentKeepalive = it)) }, + label = { + Text( + stringResource(R.string.persistent_keepalive), + style = MaterialTheme.typography.bodyMedium, + ) + }, + placeholder = { + Text(stringResource(R.string.optional), style = MaterialTheme.typography.bodyMedium) + }, + trailingIcon = { + Text( + stringResource(R.string.seconds), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(end = 10.dp), + ) + }, + modifier = Modifier.fillMaxWidth(), + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + ) + ConfigurationTextBox( + value = peer.endpoint, + onValueChange = { onPeerChange(peer.copy(endpoint = it)) }, + label = stringResource(R.string.endpoint), + hint = stringResource(R.string.endpoint).lowercase(), + modifier = Modifier.fillMaxWidth(), + ) + OutlinedTextField( + value = peer.allowedIps, + onValueChange = { onPeerChange(peer.copy(allowedIps = it)) }, + label = { + Text(stringResource(R.string.allowed_ips), style = MaterialTheme.typography.bodyMedium) + }, + placeholder = { + Text( + stringResource(R.string.comma_separated_list), + style = MaterialTheme.typography.bodyMedium, + ) + }, + modifier = Modifier.fillMaxWidth(), + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/components/PeersSection.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/components/PeersSection.kt index eabcec11..d6d732e4 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/components/PeersSection.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/components/PeersSection.kt @@ -37,65 +37,74 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize @Composable fun PeersSection(uiState: ConfigUiState, viewModel: ConfigViewModel) { - uiState.configProxy.peers.forEachIndexed { index, peer -> - var isDropDownExpanded by remember { mutableStateOf(false) } + uiState.configProxy.peers.forEachIndexed { index, peer -> + var isDropDownExpanded by remember { mutableStateOf(false) } - Surface( - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surface, - ) { - Column( - verticalArrangement = Arrangement.spacedBy(6.dp), - modifier = Modifier.padding(16.dp).focusGroup(), - ) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), - ) { - GroupLabel(stringResource(R.string.peer)) - Row( - horizontalArrangement = Arrangement.spacedBy(10.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - IconButton( - modifier = Modifier.size(iconSize), - onClick = { viewModel.removePeer(index) }, - ) { - Icon(Icons.Rounded.Delete, contentDescription = stringResource(R.string.delete)) - } - Column { - IconButton( - modifier = Modifier.size(iconSize), - onClick = { isDropDownExpanded = true }, - ) { - Icon(Icons.Rounded.MoreVert, contentDescription = stringResource(R.string.quick_actions)) - } - DropdownMenu( - expanded = isDropDownExpanded, - onDismissRequest = { isDropDownExpanded = false }, - modifier = Modifier.shadow(12.dp).background(MaterialTheme.colorScheme.surface), - ) { - DropdownMenuItem( - text = { - Text(if (peer.isLanExcluded()) stringResource(R.string.include_lan) else stringResource(R.string.exclude_lan)) - }, - onClick = { - viewModel.toggleLanExclusion(index) - isDropDownExpanded = false - }, - ) - } - } - } - } - PeerFields( - peer = peer, - onPeerChange = { viewModel.updatePeer(index, it) }, - showAuthPrompt = { viewModel.toggleShowAuthPrompt() }, - isAuthenticated = uiState.isAuthenticated, - ) - } - } - } + Surface(shape = RoundedCornerShape(12.dp), color = MaterialTheme.colorScheme.surface) { + Column( + verticalArrangement = Arrangement.spacedBy(6.dp), + modifier = Modifier.padding(16.dp).focusGroup(), + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + GroupLabel(stringResource(R.string.peer)) + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + IconButton( + modifier = Modifier.size(iconSize), + onClick = { viewModel.removePeer(index) }, + ) { + Icon( + Icons.Rounded.Delete, + contentDescription = stringResource(R.string.delete), + ) + } + Column { + IconButton( + modifier = Modifier.size(iconSize), + onClick = { isDropDownExpanded = true }, + ) { + Icon( + Icons.Rounded.MoreVert, + contentDescription = stringResource(R.string.quick_actions), + ) + } + DropdownMenu( + expanded = isDropDownExpanded, + onDismissRequest = { isDropDownExpanded = false }, + modifier = + Modifier.shadow(12.dp) + .background(MaterialTheme.colorScheme.surface), + ) { + DropdownMenuItem( + text = { + Text( + if (peer.isLanExcluded()) + stringResource(R.string.include_lan) + else stringResource(R.string.exclude_lan) + ) + }, + onClick = { + viewModel.toggleLanExclusion(index) + isDropDownExpanded = false + }, + ) + } + } + } + } + PeerFields( + peer = peer, + onPeerChange = { viewModel.updatePeer(index, it) }, + showAuthPrompt = { viewModel.toggleShowAuthPrompt() }, + isAuthenticated = uiState.isAuthenticated, + ) + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/state/ConfigUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/state/ConfigUiState.kt index 3515eae5..6dc872bc 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/state/ConfigUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/state/ConfigUiState.kt @@ -6,12 +6,13 @@ import com.zaneschepke.wireguardautotunnel.ui.state.PeerProxy import com.zaneschepke.wireguardautotunnel.util.StringValue data class ConfigUiState( - val tunnelName: String = "", - val configProxy: ConfigProxy = ConfigProxy(`interface` = InterfaceProxy(), peers = listOf(PeerProxy())), - val showAmneziaValues: Boolean = false, - val showScripts: Boolean = false, - val isAuthenticated: Boolean = true, - val showAuthPrompt: Boolean = false, - val message: StringValue? = null, - val success: Boolean? = null, + val tunnelName: String = "", + val configProxy: ConfigProxy = + ConfigProxy(`interface` = InterfaceProxy(), peers = listOf(PeerProxy())), + val showAmneziaValues: Boolean = false, + val showScripts: Boolean = false, + val isAuthenticated: Boolean = true, + val showAuthPrompt: Boolean = false, + val message: StringValue? = null, + val success: Boolean? = null, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/scanner/ScannerScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/scanner/ScannerScreen.kt index 7a7c1a5b..ba2312ce 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/scanner/ScannerScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/scanner/ScannerScreen.kt @@ -12,24 +12,22 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable fun ScannerScreen(viewModel: AppViewModel) { - val context = LocalContext.current + val context = LocalContext.current - val barcodeView = remember { - CompoundBarcodeView(context).apply { - this.initializeFromIntent((context as Activity).intent) - this.setStatusText("") - this.decodeSingle { result -> - result.text?.let { barCodeOrQr -> - viewModel.handleEvent(AppEvent.ImportTunnelFromQrCode(barCodeOrQr)) - } - } - } - } - AndroidView(factory = { barcodeView }) - DisposableEffect(Unit) { - barcodeView.resume() - onDispose { - barcodeView.pause() - } - } + val barcodeView = remember { + CompoundBarcodeView(context).apply { + this.initializeFromIntent((context as Activity).intent) + this.setStatusText("") + this.decodeSingle { result -> + result.text?.let { barCodeOrQr -> + viewModel.handleEvent(AppEvent.ImportTunnelFromQrCode(barCodeOrQr)) + } + } + } + } + AndroidView(factory = { barcodeView }) + DisposableEffect(Unit) { + barcodeView.resume() + onDispose { barcodeView.pause() } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/SplitTunnelScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/SplitTunnelScreen.kt index ff44fb70..3025ed1c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/SplitTunnelScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/SplitTunnelScreen.kt @@ -16,35 +16,39 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable -fun SplitTunnelScreen(appViewModel: AppViewModel, viewModel: SplitTunnelViewModel = hiltViewModel()) { - val uiState by viewModel.uiState.collectAsStateWithLifecycle() +fun SplitTunnelScreen( + appViewModel: AppViewModel, + viewModel: SplitTunnelViewModel = hiltViewModel(), +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() - LaunchedEffect(Unit) { - appViewModel.handleEvent(AppEvent.SetScreenAction { viewModel.saveChanges() }) - } + LaunchedEffect(Unit) { + appViewModel.handleEvent(AppEvent.SetScreenAction { viewModel.saveChanges() }) + } - LaunchedEffect(uiState.success) { - if (uiState.success == true) { - appViewModel.handleEvent(AppEvent.ShowMessage(StringValue.StringResource(R.string.config_changes_saved))) - appViewModel.handleEvent(AppEvent.PopBackStack(true)) - } - } + LaunchedEffect(uiState.success) { + if (uiState.success == true) { + appViewModel.handleEvent( + AppEvent.ShowMessage(StringValue.StringResource(R.string.config_changes_saved)) + ) + appViewModel.handleEvent(AppEvent.PopBackStack(true)) + } + } - Crossfade( - targetState = uiState.loading, - animationSpec = tween(200), - modifier = Modifier - .fillMaxSize(), - ) { isLoading -> - if (isLoading) { - SplitTunnelSkeleton() - } else { - SplitTunnelContent( - uiState = uiState, - onSplitOptionChange = viewModel::updateSplitOption, - onAppSelectionToggle = viewModel::toggleAppSelection, - onQueryChange = viewModel::onSearchQuery, - ) - } - } + Crossfade( + targetState = uiState.loading, + animationSpec = tween(200), + modifier = Modifier.fillMaxSize(), + ) { isLoading -> + if (isLoading) { + SplitTunnelSkeleton() + } else { + SplitTunnelContent( + uiState = uiState, + onSplitOptionChange = viewModel::updateSplitOption, + onAppSelectionToggle = viewModel::toggleAppSelection, + onQueryChange = viewModel::onSearchQuery, + ) + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/SplitTunnelSkeleton.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/SplitTunnelSkeleton.kt index 7bad9cb1..cd7e8bd1 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/SplitTunnelSkeleton.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/SplitTunnelSkeleton.kt @@ -25,85 +25,68 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize @Composable fun SplitTunnelSkeleton() { - val shimmerBrush = ShimmerEffect() + val shimmerBrush = ShimmerEffect() - Column( - verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.CenterVertically), - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxWidth() - .padding(top = 24.dp), - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp) - .height(45.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - repeat(3) { - Box( - modifier = Modifier - .weight(1f) - .height(45.dp) - .clip(RoundedCornerShape(8.dp)) - .background(shimmerBrush), - ) - } - } + Column( + verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth().padding(top = 24.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp).height(45.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + repeat(3) { + Box( + modifier = + Modifier.weight(1f) + .height(45.dp) + .clip(RoundedCornerShape(8.dp)) + .background(shimmerBrush) + ) + } + } - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp) - .height(45.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Box( - modifier = Modifier - .height(45.dp) - .fillMaxWidth() - .clip(RoundedCornerShape(8.dp)) - .background(shimmerBrush), - ) - } + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp).height(45.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Box( + modifier = + Modifier.height(45.dp) + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .background(shimmerBrush) + ) + } - LazyColumn( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top, - contentPadding = PaddingValues(top = 10.dp), - modifier = Modifier.fillMaxWidth(), - ) { - items(20) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Box( - modifier = Modifier - .size(iconSize) - .clip(CircleShape) - .background(shimmerBrush), - ) - Spacer(modifier = Modifier.width(16.dp)) - Box( - modifier = Modifier - .height(20.dp) - .weight(1f) - .clip(RoundedCornerShape(4.dp)) - .background(shimmerBrush), - ) - Spacer(modifier = Modifier.width(16.dp)) - Box( - modifier = Modifier - .size(24.dp) - .clip(CircleShape) - .background(shimmerBrush), - ) - } - } - } - } + LazyColumn( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + contentPadding = PaddingValues(top = 10.dp), + modifier = Modifier.fillMaxWidth(), + ) { + items(20) { + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = + Modifier.size(iconSize).clip(CircleShape).background(shimmerBrush) + ) + Spacer(modifier = Modifier.width(16.dp)) + Box( + modifier = + Modifier.height(20.dp) + .weight(1f) + .clip(RoundedCornerShape(4.dp)) + .background(shimmerBrush) + ) + Spacer(modifier = Modifier.width(16.dp)) + Box(modifier = Modifier.size(24.dp).clip(CircleShape).background(shimmerBrush)) + } + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/SplitTunnelViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/SplitTunnelViewModel.kt index 351145f1..53c609bd 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/SplitTunnelViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/SplitTunnelViewModel.kt @@ -15,6 +15,9 @@ import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy import com.zaneschepke.wireguardautotunnel.util.extensions.getAllInternetCapablePackages import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import java.text.Collator +import java.util.* +import javax.inject.Inject import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -22,140 +25,154 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import java.text.Collator -import java.util.* -import javax.inject.Inject @HiltViewModel -class SplitTunnelViewModel @Inject constructor( - @ApplicationContext private val context: Context, - private val tunnelRepository: TunnelRepository, - savedStateHandle: SavedStateHandle, +class SplitTunnelViewModel +@Inject +constructor( + @ApplicationContext private val context: Context, + private val tunnelRepository: TunnelRepository, + savedStateHandle: SavedStateHandle, ) : ViewModel() { - private val _uiState = MutableStateFlow(SplitTunnelUiState()) - val uiState: StateFlow = _uiState - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000L), - initialValue = SplitTunnelUiState(), - ) + private val _uiState = MutableStateFlow(SplitTunnelUiState()) + val uiState: StateFlow = + _uiState.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000L), + initialValue = SplitTunnelUiState(), + ) - private val tunnelId: Int? = savedStateHandle.get(Route.SplitTunnel.KEY_ID) - private var allTunneledApps: List> = emptyList() + private val tunnelId: Int? = savedStateHandle.get(Route.SplitTunnel.KEY_ID) + private var allTunneledApps: List> = emptyList() - init { - tunnelId?.let { loadInitialState(it) } - } + init { + tunnelId?.let { loadInitialState(it) } + } - private fun loadInitialState(tunnelId: Int) = viewModelScope.launch { - val tunnel = tunnelRepository.getById(tunnelId) ?: return@launch - val proxyInterface = InterfaceProxy.from(tunnel.toAmConfig().`interface`) - val splitOption = when { - proxyInterface.excludedApplications.isNotEmpty() -> SplitOption.EXCLUDE - proxyInterface.includedApplications.isNotEmpty() -> SplitOption.INCLUDE - else -> SplitOption.ALL - } + private fun loadInitialState(tunnelId: Int) = + viewModelScope.launch { + val tunnel = tunnelRepository.getById(tunnelId) ?: return@launch + val proxyInterface = InterfaceProxy.from(tunnel.toAmConfig().`interface`) + val splitOption = + when { + proxyInterface.excludedApplications.isNotEmpty() -> SplitOption.EXCLUDE + proxyInterface.includedApplications.isNotEmpty() -> SplitOption.INCLUDE + else -> SplitOption.ALL + } - val packages = context.getAllInternetCapablePackages() + val packages = context.getAllInternetCapablePackages() - val installedPackages = packages - .map { it.packageName } - .toSet() + val installedPackages = packages.map { it.packageName }.toSet() - // remove uninstalled apps - proxyInterface.includedApplications.retainAll { it in installedPackages } - proxyInterface.excludedApplications.retainAll { it in installedPackages } + // remove uninstalled apps + proxyInterface.includedApplications.retainAll { it in installedPackages } + proxyInterface.excludedApplications.retainAll { it in installedPackages } - var configProxy = ConfigProxy.from(tunnel.toAmConfig()) - configProxy = configProxy.copy(`interface` = proxyInterface) - saveProxyConfig(configProxy, tunnel) + var configProxy = ConfigProxy.from(tunnel.toAmConfig()) + configProxy = configProxy.copy(`interface` = proxyInterface) + saveProxyConfig(configProxy, tunnel) - val collator = Collator.getInstance(Locale.getDefault()) - val tunneledApps = packages - .filter { it.applicationInfo != null } - .map { pack -> - val selected = when (splitOption) { - SplitOption.INCLUDE -> proxyInterface.includedApplications.contains(pack.packageName) - SplitOption.ALL -> false - SplitOption.EXCLUDE -> proxyInterface.excludedApplications.contains(pack.packageName) - } - Pair( - TunnelApp( - name = context.packageManager.getApplicationLabel(pack.applicationInfo!!).toString(), - `package` = pack.packageName, - ), - selected, - ) - }.sortedWith(compareBy(collator) { it.first.name }) + val collator = Collator.getInstance(Locale.getDefault()) + val tunneledApps = + packages + .filter { it.applicationInfo != null } + .map { pack -> + val selected = + when (splitOption) { + SplitOption.INCLUDE -> + proxyInterface.includedApplications.contains(pack.packageName) + SplitOption.ALL -> false + SplitOption.EXCLUDE -> + proxyInterface.excludedApplications.contains(pack.packageName) + } + Pair( + TunnelApp( + name = + context.packageManager + .getApplicationLabel(pack.applicationInfo!!) + .toString(), + `package` = pack.packageName, + ), + selected, + ) + } + .sortedWith(compareBy(collator) { it.first.name }) - allTunneledApps = tunneledApps + allTunneledApps = tunneledApps - delay(500) + delay(500) - _uiState.update { - SplitTunnelUiState( - loading = false, - tunnelConf = tunnel, - tunneledApps = tunneledApps, - splitOption = splitOption, - ) - } - } + _uiState.update { + SplitTunnelUiState( + loading = false, + tunnelConf = tunnel, + tunneledApps = tunneledApps, + splitOption = splitOption, + ) + } + } - fun onSearchQuery(query: String) { - val filteredApps = if (query.isBlank()) { - allTunneledApps - } else { - allTunneledApps.filter { - it.first.name.contains(query, ignoreCase = true) || - it.first.`package`.contains(query, ignoreCase = true) - } - } - _uiState.update { - it.copy( - searchQuery = query, - tunneledApps = filteredApps, - ) - } - } + fun onSearchQuery(query: String) { + val filteredApps = + if (query.isBlank()) { + allTunneledApps + } else { + allTunneledApps.filter { + it.first.name.contains(query, ignoreCase = true) || + it.first.`package`.contains(query, ignoreCase = true) + } + } + _uiState.update { it.copy(searchQuery = query, tunneledApps = filteredApps) } + } - fun updateSplitOption(newOption: SplitOption) { - _uiState.value = _uiState.value.copy(splitOption = newOption) - } + fun updateSplitOption(newOption: SplitOption) { + _uiState.value = _uiState.value.copy(splitOption = newOption) + } - fun toggleAppSelection(packageName: String) { - val currentState = _uiState.value - val updatedApps = currentState.tunneledApps.map { (app, selected) -> - if (app.`package` == packageName) Pair(app, !selected) else Pair(app, selected) - } - _uiState.value = currentState.copy(tunneledApps = updatedApps) - } + fun toggleAppSelection(packageName: String) { + val currentState = _uiState.value + val updatedApps = + currentState.tunneledApps.map { (app, selected) -> + if (app.`package` == packageName) Pair(app, !selected) else Pair(app, selected) + } + _uiState.value = currentState.copy(tunneledApps = updatedApps) + } - fun saveChanges() = viewModelScope.launch { - val state = _uiState.value - val tunnel = state.tunnelConf ?: return@launch - val configProxy = ConfigProxy.from(tunnel.toAmConfig()) - val updatedApps = state.tunneledApps - with(configProxy.`interface`) { - includedApplications.clear() - excludedApplications.clear() - when (state.splitOption) { - SplitOption.INCLUDE -> { - includedApplications.addAll(updatedApps.filter { it.second }.map { it.first.`package` }) - } - SplitOption.EXCLUDE -> { - excludedApplications.addAll(updatedApps.filter { it.second }.map { it.first.`package` }) - } - SplitOption.ALL -> Unit - } - } - saveProxyConfig(configProxy, tunnel) - _uiState.update { it.copy(success = true) } - } + fun saveChanges() = + viewModelScope.launch { + val state = _uiState.value + val tunnel = state.tunnelConf ?: return@launch + val configProxy = ConfigProxy.from(tunnel.toAmConfig()) + val updatedApps = state.tunneledApps + with(configProxy.`interface`) { + includedApplications.clear() + excludedApplications.clear() + when (state.splitOption) { + SplitOption.INCLUDE -> { + includedApplications.addAll( + updatedApps.filter { it.second }.map { it.first.`package` } + ) + } + SplitOption.EXCLUDE -> { + excludedApplications.addAll( + updatedApps.filter { it.second }.map { it.first.`package` } + ) + } + SplitOption.ALL -> Unit + } + } + saveProxyConfig(configProxy, tunnel) + _uiState.update { it.copy(success = true) } + } - private suspend fun saveProxyConfig(proxy: ConfigProxy, tunnel: TunnelConf) { - val (wg, am) = proxy.buildConfigs() - tunnelRepository.save(tunnel.copyWithCallback(amQuick = am.toAwgQuickString(true), wgQuick = wg.toWgQuickString(true))) - } + private suspend fun saveProxyConfig(proxy: ConfigProxy, tunnel: TunnelConf) { + val (wg, am) = proxy.buildConfigs() + tunnelRepository.save( + tunnel.copyWithCallback( + amQuick = am.toAwgQuickString(true), + wgQuick = wg.toWgQuickString(true), + ) + ) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/components/AppListItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/components/AppListItem.kt index 32d974ec..9b17eaaa 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/components/AppListItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/components/AppListItem.kt @@ -21,38 +21,34 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize @Composable fun AppListItem(appInfo: TunnelApp, isSelected: Boolean, onToggle: () -> Unit) { - val context = LocalContext.current - val icon = remember(appInfo.`package`) { - try { - context.packageManager.getApplicationIcon(appInfo.`package`) - } catch (e: PackageManager.NameNotFoundException) { - null - } - } + val context = LocalContext.current + val icon = + remember(appInfo.`package`) { + try { + context.packageManager.getApplicationIcon(appInfo.`package`) + } catch (e: PackageManager.NameNotFoundException) { + null + } + } - SelectionItemButton( - leading = { - Image( - painter = rememberDrawablePainter(icon), - contentDescription = appInfo.name, - modifier = Modifier - .padding(horizontal = 24.dp) - .size(iconSize), - ) - }, - buttonText = appInfo.name, - onClick = onToggle, - trailing = { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End, - verticalAlignment = Alignment.CenterVertically, - ) { - Checkbox( - checked = isSelected, - onCheckedChange = { onToggle() }, - ) - } - }, - ) + SelectionItemButton( + leading = { + Image( + painter = rememberDrawablePainter(icon), + contentDescription = appInfo.name, + modifier = Modifier.padding(horizontal = 24.dp).size(iconSize), + ) + }, + buttonText = appInfo.name, + onClick = onToggle, + trailing = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + Checkbox(checked = isSelected, onCheckedChange = { onToggle() }) + } + }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/components/AppListSection.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/components/AppListSection.kt index 0d729f1f..7cbcb114 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/components/AppListSection.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/components/AppListSection.kt @@ -28,49 +28,51 @@ import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField import com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.state.TunnelApp @Composable -fun AppListSection(apps: List>, onAppSelectionToggle: (String) -> Unit, onQueryChange: (String) -> Unit, query: String) { - val inputHeight = 45.dp +fun AppListSection( + apps: List>, + onAppSelectionToggle: (String) -> Unit, + onQueryChange: (String) -> Unit, + query: String, +) { + val inputHeight = 45.dp - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxWidth(), - ) { - CustomTextField( - textStyle = MaterialTheme.typography.labelMedium.copy( - color = MaterialTheme.colorScheme.onBackground, - ), - value = query, - onValueChange = onQueryChange, - interactionSource = remember { MutableInteractionSource() }, - label = {}, - leading = { - Icon(Icons.Outlined.Search, stringResource(R.string.search)) - }, - containerColor = MaterialTheme.colorScheme.surface, - modifier = Modifier - .fillMaxWidth() - .height(inputHeight) - .padding(horizontal = 24.dp), - singleLine = true, - keyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.None, - imeAction = ImeAction.Done, - ), - keyboardActions = KeyboardActions(), - ) - LazyColumn( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top, - contentPadding = PaddingValues(top = 10.dp), - ) { - items(apps, key = { it.first.`package` }) { app -> - AppListItem( - appInfo = app.first, - isSelected = app.second, - onToggle = { onAppSelectionToggle(app.first.`package`) }, - ) - } - } - } + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth(), + ) { + CustomTextField( + textStyle = + MaterialTheme.typography.labelMedium.copy( + color = MaterialTheme.colorScheme.onBackground + ), + value = query, + onValueChange = onQueryChange, + interactionSource = remember { MutableInteractionSource() }, + label = {}, + leading = { Icon(Icons.Outlined.Search, stringResource(R.string.search)) }, + containerColor = MaterialTheme.colorScheme.surface, + modifier = Modifier.fillMaxWidth().height(inputHeight).padding(horizontal = 24.dp), + singleLine = true, + keyboardOptions = + KeyboardOptions( + capitalization = KeyboardCapitalization.None, + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions(), + ) + LazyColumn( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + contentPadding = PaddingValues(top = 10.dp), + ) { + items(apps, key = { it.first.`package` }) { app -> + AppListItem( + appInfo = app.first, + isSelected = app.second, + onToggle = { onAppSelectionToggle(app.first.`package`) }, + ) + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/components/SplitOptionsSelector.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/components/SplitOptionsSelector.kt index 031ba156..14fd80e5 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/components/SplitOptionsSelector.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/components/SplitOptionsSelector.kt @@ -25,54 +25,61 @@ import java.util.* @Composable fun SplitOptionSelector(selectedOption: SplitOption, onOptionChange: (SplitOption) -> Unit) { - val context = LocalContext.current - val inputHeight = 45.dp + val context = LocalContext.current + val inputHeight = 45.dp - MultiChoiceSegmentedButtonRow( - modifier = Modifier - .background(color = MaterialTheme.colorScheme.surface) - .fillMaxWidth() - .padding(horizontal = 24.dp) - .height(inputHeight), - ) { - SplitOption.entries.forEachIndexed { index, entry -> - val active = selectedOption == entry - SegmentedButton( - shape = SegmentedButtonDefaults.itemShape( - index = index, - count = SplitOption.entries.size, - baseShape = RoundedCornerShape(8.dp), - ), - icon = { - SegmentedButtonDefaults.Icon(active = active, activeContent = { - Icon( - imageVector = Icons.Outlined.Check, - contentDescription = stringResource(R.string.select), - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(SegmentedButtonDefaults.IconSize), - ) - }) { - Icon( - imageVector = entry.icon(), - contentDescription = entry.icon().name, - modifier = Modifier.size(SegmentedButtonDefaults.IconSize), - ) - } - }, - colors = SegmentedButtonDefaults.colors().copy( - activeContainerColor = MaterialTheme.colorScheme.surface, - inactiveContainerColor = MaterialTheme.colorScheme.surface, - ), - onCheckedChange = { onOptionChange(entry) }, - checked = active, - ) { - Text( - entry.text().asString(context) - .replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }, - color = MaterialTheme.colorScheme.onBackground, - style = MaterialTheme.typography.labelMedium, - ) - } - } - } + MultiChoiceSegmentedButtonRow( + modifier = + Modifier.background(color = MaterialTheme.colorScheme.surface) + .fillMaxWidth() + .padding(horizontal = 24.dp) + .height(inputHeight) + ) { + SplitOption.entries.forEachIndexed { index, entry -> + val active = selectedOption == entry + SegmentedButton( + shape = + SegmentedButtonDefaults.itemShape( + index = index, + count = SplitOption.entries.size, + baseShape = RoundedCornerShape(8.dp), + ), + icon = { + SegmentedButtonDefaults.Icon( + active = active, + activeContent = { + Icon( + imageVector = Icons.Outlined.Check, + contentDescription = stringResource(R.string.select), + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(SegmentedButtonDefaults.IconSize), + ) + }, + ) { + Icon( + imageVector = entry.icon(), + contentDescription = entry.icon().name, + modifier = Modifier.size(SegmentedButtonDefaults.IconSize), + ) + } + }, + colors = + SegmentedButtonDefaults.colors() + .copy( + activeContainerColor = MaterialTheme.colorScheme.surface, + inactiveContainerColor = MaterialTheme.colorScheme.surface, + ), + onCheckedChange = { onOptionChange(entry) }, + checked = active, + ) { + Text( + entry.text().asString(context).replaceFirstChar { + if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() + }, + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.labelMedium, + ) + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/components/SplitTunnelContent.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/components/SplitTunnelContent.kt index 659b4653..05abc3bd 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/components/SplitTunnelContent.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/components/SplitTunnelContent.kt @@ -13,29 +13,27 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.state.Spl @Composable fun SplitTunnelContent( - uiState: SplitTunnelUiState, - onSplitOptionChange: (SplitOption) -> Unit, - onAppSelectionToggle: (String) -> Unit, - onQueryChange: (String) -> Unit, + uiState: SplitTunnelUiState, + onSplitOptionChange: (SplitOption) -> Unit, + onAppSelectionToggle: (String) -> Unit, + onQueryChange: (String) -> Unit, ) { - Column( - verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.CenterVertically), - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxWidth() - .padding(top = 24.dp), - ) { - SplitOptionSelector( - selectedOption = uiState.splitOption, - onOptionChange = onSplitOptionChange, - ) - if (uiState.splitOption != SplitOption.ALL) { - AppListSection( - apps = uiState.tunneledApps, - onAppSelectionToggle = onAppSelectionToggle, - onQueryChange = onQueryChange, - uiState.searchQuery, - ) - } - } + Column( + verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth().padding(top = 24.dp), + ) { + SplitOptionSelector( + selectedOption = uiState.splitOption, + onOptionChange = onSplitOptionChange, + ) + if (uiState.splitOption != SplitOption.ALL) { + AppListSection( + apps = uiState.tunneledApps, + onAppSelectionToggle = onAppSelectionToggle, + onQueryChange = onQueryChange, + uiState.searchQuery, + ) + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/state/SplitOption.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/state/SplitOption.kt index 358138d8..db474fe2 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/state/SplitOption.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/state/SplitOption.kt @@ -9,24 +9,23 @@ import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.util.StringValue enum class SplitOption { - INCLUDE, - ALL, - EXCLUDE, - ; + INCLUDE, + ALL, + EXCLUDE; - fun icon(): ImageVector { - return when (this) { - ALL -> Icons.Filled.AllInclusive - INCLUDE -> Icons.Filled.Add - EXCLUDE -> Icons.Filled.Remove - } - } + fun icon(): ImageVector { + return when (this) { + ALL -> Icons.Filled.AllInclusive + INCLUDE -> Icons.Filled.Add + EXCLUDE -> Icons.Filled.Remove + } + } - fun text(): StringValue { - return when (this) { - ALL -> StringValue.StringResource(R.string.all) - INCLUDE -> StringValue.StringResource(R.string.include) - EXCLUDE -> StringValue.StringResource(R.string.exclude) - } - } + fun text(): StringValue { + return when (this) { + ALL -> StringValue.StringResource(R.string.all) + INCLUDE -> StringValue.StringResource(R.string.include) + EXCLUDE -> StringValue.StringResource(R.string.exclude) + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/state/SplitTunnelUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/state/SplitTunnelUiState.kt index 527b9219..06eaee1b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/state/SplitTunnelUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/state/SplitTunnelUiState.kt @@ -3,10 +3,10 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.state import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf data class SplitTunnelUiState( - val loading: Boolean = true, - val tunnelConf: TunnelConf? = null, - val tunneledApps: SplitTunnelApps = emptyList(), - val splitOption: SplitOption = SplitOption.ALL, - val searchQuery: String = "", - val success: Boolean? = null, + val loading: Boolean = true, + val tunnelConf: TunnelConf? = null, + val tunneledApps: SplitTunnelApps = emptyList(), + val splitOption: SplitOption = SplitOption.ALL, + val searchQuery: String = "", + val success: Boolean? = null, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/state/TunnelApp.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/state/TunnelApp.kt index d13872b9..f5add72d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/state/TunnelApp.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/splittunnel/state/TunnelApp.kt @@ -1,8 +1,5 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.state -data class TunnelApp( - val name: String, - val `package`: String, -) +data class TunnelApp(val name: String, val `package`: String) typealias SplitTunnelApps = List> diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/tunneloptions/TunnelOptionsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/tunneloptions/TunnelOptionsScreen.kt index 59ba4040..00cd97ef 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/tunneloptions/TunnelOptionsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/tunneloptions/TunnelOptionsScreen.kt @@ -24,41 +24,40 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.compone import com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.components.ServerIpv4Item import com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.components.SplitTunnelingItem import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState - import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel @Composable fun TunnelOptionsScreen(tunnelConf: TunnelConf, appUiState: AppUiState, viewModel: AppViewModel) { - var currentText by remember { mutableStateOf("") } + var currentText by remember { mutableStateOf("") } - LaunchedEffect(tunnelConf.tunnelNetworks) { - currentText = "" - } + LaunchedEffect(tunnelConf.tunnelNetworks) { currentText = "" } - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(top = 24.dp) - .padding(horizontal = 24.dp), - ) { - SurfaceSelectionGroupButton( - items = listOf( - PrimaryTunnelItem(tunnelConf, viewModel), - AutoTunnelingItem(tunnelConf), - ServerIpv4Item(tunnelConf, viewModel), - SplitTunnelingItem(tunnelConf), - ), - ) - SurfaceSelectionGroupButton( - items = buildList { - add(PingRestartItem(tunnelConf, viewModel)) - if (tunnelConf.isPingEnabled) { - add(PingConfigItem(tunnelConf, viewModel)) - } - }, - ) - } + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), + modifier = + Modifier.fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(top = 24.dp) + .padding(horizontal = 24.dp), + ) { + SurfaceSelectionGroupButton( + items = + listOf( + PrimaryTunnelItem(tunnelConf, viewModel), + AutoTunnelingItem(tunnelConf), + ServerIpv4Item(tunnelConf, viewModel), + SplitTunnelingItem(tunnelConf), + ) + ) + SurfaceSelectionGroupButton( + items = + buildList { + add(PingRestartItem(tunnelConf, viewModel)) + if (tunnelConf.isPingEnabled) { + add(PingConfigItem(tunnelConf, viewModel)) + } + } + ) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/tunneloptions/components/AutoTunnelingItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/tunneloptions/components/AutoTunnelingItem.kt index 83dc2bab..d7d362f0 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/tunneloptions/components/AutoTunnelingItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/tunneloptions/components/AutoTunnelingItem.kt @@ -15,24 +15,25 @@ import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavControll @Composable fun AutoTunnelingItem(tunnelConf: TunnelConf): SelectionItem { - val navController = LocalNavController.current - return SelectionItem( - leadingIcon = Icons.Outlined.Bolt, - title = { - Text( - text = stringResource(R.string.auto_tunneling), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - description = { - Text( - text = stringResource(R.string.tunnel_specific_settings), - style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline), - ) - }, - trailing = { - ForwardButton { navController.navigate(Route.TunnelAutoTunnel(id = tunnelConf.id)) } - }, - onClick = { navController.navigate(Route.TunnelAutoTunnel(id = tunnelConf.id)) }, - ) + val navController = LocalNavController.current + return SelectionItem( + leadingIcon = Icons.Outlined.Bolt, + title = { + Text( + text = stringResource(R.string.auto_tunneling), + style = + MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + description = { + Text( + text = stringResource(R.string.tunnel_specific_settings), + style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline), + ) + }, + trailing = { + ForwardButton { navController.navigate(Route.TunnelAutoTunnel(id = tunnelConf.id)) } + }, + onClick = { navController.navigate(Route.TunnelAutoTunnel(id = tunnelConf.id)) }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/tunneloptions/components/PingConfigItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/tunneloptions/components/PingConfigItem.kt index a2ecc71f..292d615d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/tunneloptions/components/PingConfigItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/tunneloptions/components/PingConfigItem.kt @@ -19,42 +19,54 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable fun PingConfigItem(tunnelConf: TunnelConf, viewModel: AppViewModel): SelectionItem { - return SelectionItem( - title = {}, - description = { - Column( - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - SubmitConfigurationTextBox( - value = tunnelConf.pingIp, - label = stringResource(R.string.set_custom_ping_ip), - hint = stringResource(R.string.default_ping_ip), - isErrorValue = { it?.isNotBlank() == true && !it.isValidIpv4orIpv6Address() }, - onSubmit = { ip -> viewModel.handleEvent(AppEvent.SetTunnelPingIp(tunnelConf, ip)) }, - ) - SubmitConfigurationTextBox( - value = tunnelConf.pingInterval?.let { (it / 1000).toString() }, - label = stringResource(R.string.set_custom_ping_internal), - hint = "(${stringResource(R.string.optional_default)} ${Constants.PING_INTERVAL / 1000})", - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Number, - imeAction = ImeAction.Done, - ), - isErrorValue = { it?.toLongOrNull()?.let { value -> value >= Long.MAX_VALUE / 1000 } ?: false }, - onSubmit = { interval -> viewModel.handleEvent(AppEvent.SetTunnelPingInterval(tunnelConf, interval)) }, - ) - SubmitConfigurationTextBox( - value = tunnelConf.pingCooldown?.let { (it / 1000).toString() }, - label = stringResource(R.string.set_custom_ping_cooldown), - hint = "(${stringResource(R.string.optional_default)} ${Constants.PING_COOLDOWN / 1000})", - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Number, - imeAction = ImeAction.Done, - ), - isErrorValue = { it?.toLongOrNull()?.let { value -> value >= Long.MAX_VALUE / 1000 } ?: false }, - onSubmit = { cooldown -> viewModel.handleEvent(AppEvent.SetTunnelPingCooldown(tunnelConf, cooldown)) }, - ) - } - }, - ) + return SelectionItem( + title = {}, + description = { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + SubmitConfigurationTextBox( + value = tunnelConf.pingIp, + label = stringResource(R.string.set_custom_ping_ip), + hint = stringResource(R.string.default_ping_ip), + isErrorValue = { it?.isNotBlank() == true && !it.isValidIpv4orIpv6Address() }, + onSubmit = { ip -> + viewModel.handleEvent(AppEvent.SetTunnelPingIp(tunnelConf, ip)) + }, + ) + SubmitConfigurationTextBox( + value = tunnelConf.pingInterval?.let { (it / 1000).toString() }, + label = stringResource(R.string.set_custom_ping_internal), + hint = + "(${stringResource(R.string.optional_default)} ${Constants.PING_INTERVAL / 1000})", + keyboardOptions = + KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Done, + ), + isErrorValue = { + it?.toLongOrNull()?.let { value -> value >= Long.MAX_VALUE / 1000 } ?: false + }, + onSubmit = { interval -> + viewModel.handleEvent(AppEvent.SetTunnelPingInterval(tunnelConf, interval)) + }, + ) + SubmitConfigurationTextBox( + value = tunnelConf.pingCooldown?.let { (it / 1000).toString() }, + label = stringResource(R.string.set_custom_ping_cooldown), + hint = + "(${stringResource(R.string.optional_default)} ${Constants.PING_COOLDOWN / 1000})", + keyboardOptions = + KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Done, + ), + isErrorValue = { + it?.toLongOrNull()?.let { value -> value >= Long.MAX_VALUE / 1000 } ?: false + }, + onSubmit = { cooldown -> + viewModel.handleEvent(AppEvent.SetTunnelPingCooldown(tunnelConf, cooldown)) + }, + ) + } + }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/tunneloptions/components/PingRestartItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/tunneloptions/components/PingRestartItem.kt index 47d9ad97..ab21a667 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/tunneloptions/components/PingRestartItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/tunneloptions/components/PingRestartItem.kt @@ -15,20 +15,21 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable fun PingRestartItem(tunnelConf: TunnelConf, viewModel: AppViewModel): SelectionItem { - return SelectionItem( - leadingIcon = Icons.Outlined.NetworkPing, - title = { - Text( - text = stringResource(R.string.restart_on_ping), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - trailing = { - ScaledSwitch( - checked = tunnelConf.isPingEnabled, - onClick = { viewModel.handleEvent(AppEvent.TogglePingTunnelEnabled(tunnelConf)) }, - ) - }, - onClick = { viewModel.handleEvent(AppEvent.TogglePingTunnelEnabled(tunnelConf)) }, - ) + return SelectionItem( + leadingIcon = Icons.Outlined.NetworkPing, + title = { + Text( + text = stringResource(R.string.restart_on_ping), + style = + MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + trailing = { + ScaledSwitch( + checked = tunnelConf.isPingEnabled, + onClick = { viewModel.handleEvent(AppEvent.TogglePingTunnelEnabled(tunnelConf)) }, + ) + }, + onClick = { viewModel.handleEvent(AppEvent.TogglePingTunnelEnabled(tunnelConf)) }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/tunneloptions/components/PrimaryTunnelItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/tunneloptions/components/PrimaryTunnelItem.kt index c9040f0f..f2171f8b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/tunneloptions/components/PrimaryTunnelItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/tunneloptions/components/PrimaryTunnelItem.kt @@ -15,26 +15,27 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable fun PrimaryTunnelItem(tunnelConf: TunnelConf, viewModel: AppViewModel): SelectionItem { - return SelectionItem( - leadingIcon = Icons.Outlined.Star, - title = { - Text( - text = stringResource(R.string.primary_tunnel), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - description = { - Text( - text = stringResource(R.string.set_primary_tunnel), - style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline), - ) - }, - trailing = { - ScaledSwitch( - checked = tunnelConf.isPrimaryTunnel, - onClick = { viewModel.handleEvent(AppEvent.TogglePrimaryTunnel(tunnelConf)) }, - ) - }, - onClick = { viewModel.handleEvent(AppEvent.TogglePrimaryTunnel(tunnelConf)) }, - ) + return SelectionItem( + leadingIcon = Icons.Outlined.Star, + title = { + Text( + text = stringResource(R.string.primary_tunnel), + style = + MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + description = { + Text( + text = stringResource(R.string.set_primary_tunnel), + style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline), + ) + }, + trailing = { + ScaledSwitch( + checked = tunnelConf.isPrimaryTunnel, + onClick = { viewModel.handleEvent(AppEvent.TogglePrimaryTunnel(tunnelConf)) }, + ) + }, + onClick = { viewModel.handleEvent(AppEvent.TogglePrimaryTunnel(tunnelConf)) }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/tunneloptions/components/ServerIpv4Item.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/tunneloptions/components/ServerIpv4Item.kt index ebe7537c..3d54bc89 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/tunneloptions/components/ServerIpv4Item.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/tunneloptions/components/ServerIpv4Item.kt @@ -15,26 +15,27 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable fun ServerIpv4Item(tunnelConf: TunnelConf, viewModel: AppViewModel): SelectionItem { - return SelectionItem( - leadingIcon = Icons.Outlined.Dns, - title = { - Text( - text = stringResource(R.string.server_ipv4), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - description = { - Text( - text = stringResource(R.string.prefer_ipv4), - style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline), - ) - }, - trailing = { - ScaledSwitch( - checked = tunnelConf.isIpv4Preferred, - onClick = { viewModel.handleEvent(AppEvent.ToggleIpv4Preferred(tunnelConf)) }, - ) - }, - onClick = { viewModel.handleEvent(AppEvent.ToggleIpv4Preferred(tunnelConf)) }, - ) + return SelectionItem( + leadingIcon = Icons.Outlined.Dns, + title = { + Text( + text = stringResource(R.string.server_ipv4), + style = + MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + description = { + Text( + text = stringResource(R.string.prefer_ipv4), + style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline), + ) + }, + trailing = { + ScaledSwitch( + checked = tunnelConf.isIpv4Preferred, + onClick = { viewModel.handleEvent(AppEvent.ToggleIpv4Preferred(tunnelConf)) }, + ) + }, + onClick = { viewModel.handleEvent(AppEvent.ToggleIpv4Preferred(tunnelConf)) }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/tunneloptions/components/SplitTunnelingItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/tunneloptions/components/SplitTunnelingItem.kt index e3f308fe..49e8539f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/tunneloptions/components/SplitTunnelingItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/tunneloptions/components/SplitTunnelingItem.kt @@ -15,18 +15,19 @@ import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavControll @Composable fun SplitTunnelingItem(tunnelConf: TunnelConf): SelectionItem { - val navController = LocalNavController.current - return SelectionItem( - leadingIcon = Icons.AutoMirrored.Outlined.CallSplit, - title = { - Text( - text = stringResource(R.string.splt_tunneling), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - trailing = { - ForwardButton { navController.navigate(Route.SplitTunnel(id = tunnelConf.id)) } - }, - onClick = { navController.navigate(Route.SplitTunnel(id = tunnelConf.id)) }, - ) + val navController = LocalNavController.current + return SelectionItem( + leadingIcon = Icons.AutoMirrored.Outlined.CallSplit, + title = { + Text( + text = stringResource(R.string.splt_tunneling), + style = + MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + trailing = { + ForwardButton { navController.navigate(Route.SplitTunnel(id = tunnelConf.id)) } + }, + onClick = { navController.navigate(Route.SplitTunnel(id = tunnelConf.id)) }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/pin/PinLockScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/pin/PinLockScreen.kt index b5a2ae82..8042668a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/pin/PinLockScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/pin/PinLockScreen.kt @@ -16,44 +16,46 @@ import xyz.teamgravity.pin_lock_compose.PinLock @Composable fun PinLockScreen(viewModel: AppViewModel) { - val context = LocalContext.current - val navController = LocalNavController.current - PinLock( - title = { pinExists -> - Text( - color = MaterialTheme.colorScheme.onSurface, - text = - if (pinExists) { - stringResource(id = R.string.enter_pin) - } else { - stringResource( - id = R.string.create_pin, - ) - }, - ) - }, - backgroundColor = MaterialTheme.colorScheme.surface, - textColor = MaterialTheme.colorScheme.onSurface, - onPinCorrect = { - // pin is correct, navigate or hide pin lock - if (context.isRunningOnTv()) { - navController.navigate(Route.Main) - } else { - val isPopped = navController.popBackStack() - if (!isPopped) { - navController.navigate(Route.Main) - } - } - }, - onPinIncorrect = { - // pin is incorrect, show error - viewModel.handleEvent(AppEvent.ShowMessage(StringValue.StringResource(R.string.incorrect_pin))) - }, - onPinCreated = { - // pin created for the first time, navigate or hide pin lock - viewModel.handleEvent(AppEvent.ShowMessage(StringValue.StringResource(R.string.pin_created))) + val context = LocalContext.current + val navController = LocalNavController.current + PinLock( + title = { pinExists -> + Text( + color = MaterialTheme.colorScheme.onSurface, + text = + if (pinExists) { + stringResource(id = R.string.enter_pin) + } else { + stringResource(id = R.string.create_pin) + }, + ) + }, + backgroundColor = MaterialTheme.colorScheme.surface, + textColor = MaterialTheme.colorScheme.onSurface, + onPinCorrect = { + // pin is correct, navigate or hide pin lock + if (context.isRunningOnTv()) { + navController.navigate(Route.Main) + } else { + val isPopped = navController.popBackStack() + if (!isPopped) { + navController.navigate(Route.Main) + } + } + }, + onPinIncorrect = { + // pin is incorrect, show error + viewModel.handleEvent( + AppEvent.ShowMessage(StringValue.StringResource(R.string.incorrect_pin)) + ) + }, + onPinCreated = { + // pin created for the first time, navigate or hide pin lock + viewModel.handleEvent( + AppEvent.ShowMessage(StringValue.StringResource(R.string.pin_created)) + ) - viewModel.handleEvent(AppEvent.TogglePinLock) - }, - ) + viewModel.handleEvent(AppEvent.TogglePinLock) + }, + ) } 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 7183fa98..2e05cd0a 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 @@ -32,65 +32,69 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.Restar import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState import com.zaneschepke.wireguardautotunnel.ui.state.AppViewState import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv - import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel @Composable fun SettingsScreen(uiState: AppUiState, appViewState: AppViewState, viewModel: AppViewModel) { - val context = LocalContext.current - val focusManager = LocalFocusManager.current - val navController = LocalNavController.current - val isRunningOnTv = remember { context.isRunningOnTv() } - val interactionSource = remember { MutableInteractionSource() } + val context = LocalContext.current + val focusManager = LocalFocusManager.current + val navController = LocalNavController.current + val isRunningOnTv = remember { context.isRunningOnTv() } + val interactionSource = remember { MutableInteractionSource() } - if (appViewState.showBottomSheet) { - ExportTunnelsBottomSheet(viewModel, isRunningOnTv) - } + if (appViewState.showBottomSheet) { + ExportTunnelsBottomSheet(viewModel, isRunningOnTv) + } - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), - modifier = Modifier - .verticalScroll(rememberScrollState()) - .fillMaxSize() - .padding(top = 24.dp) - .padding(bottom = 40.dp) - .padding(horizontal = 24.dp) - .then( - if (!isRunningOnTv) { - Modifier.clickable( - indication = null, - interactionSource = interactionSource, - onClick = { focusManager.clearFocus() }, - ) - } else { - Modifier - }, - ), - ) { - SurfaceSelectionGroupButton( - items = buildList { - add(AppShortcutsItem(uiState, viewModel)) - if (!isRunningOnTv) add(AlwaysOnVpnItem(uiState, viewModel)) - add(KillSwitchItem()) - add(RestartAtBootItem(uiState, viewModel)) - }, - ) - SurfaceSelectionGroupButton( - items = buildList { - add(AppearanceItem()) - add(LocalLoggingItem(uiState, viewModel)) - if (uiState.appState.isLocalLogsEnabled) add(ReadLogsItem()) - add(PinLockItem(uiState, viewModel)) - }, - ) - if (!isRunningOnTv) { - SurfaceSelectionGroupButton( - items = listOf(KernelModeItem(uiState, viewModel)), - ) - } - SurfaceSelectionGroupButton( - items = listOf(AdvancedSettingsItem(onClick = { navController.navigate(Route.SettingsAdvanced) })), - ) - } + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), + modifier = + Modifier.verticalScroll(rememberScrollState()) + .fillMaxSize() + .padding(top = 24.dp) + .padding(bottom = 40.dp) + .padding(horizontal = 24.dp) + .then( + if (!isRunningOnTv) { + Modifier.clickable( + indication = null, + interactionSource = interactionSource, + onClick = { focusManager.clearFocus() }, + ) + } else { + Modifier + } + ), + ) { + SurfaceSelectionGroupButton( + items = + buildList { + add(AppShortcutsItem(uiState, viewModel)) + if (!isRunningOnTv) add(AlwaysOnVpnItem(uiState, viewModel)) + add(KillSwitchItem()) + add(RestartAtBootItem(uiState, viewModel)) + } + ) + SurfaceSelectionGroupButton( + items = + buildList { + add(AppearanceItem()) + add(LocalLoggingItem(uiState, viewModel)) + if (uiState.appState.isLocalLogsEnabled) add(ReadLogsItem()) + add(PinLockItem(uiState, viewModel)) + } + ) + if (!isRunningOnTv) { + SurfaceSelectionGroupButton(items = listOf(KernelModeItem(uiState, viewModel))) + } + SurfaceSelectionGroupButton( + items = + listOf( + AdvancedSettingsItem( + onClick = { navController.navigate(Route.SettingsAdvanced) } + ) + ) + ) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/advanced/SettingsAdvancedScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/advanced/SettingsAdvancedScreen.kt index 5e96eaca..3d9e424a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/advanced/SettingsAdvancedScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/advanced/SettingsAdvancedScreen.kt @@ -21,34 +21,30 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel @Composable fun SettingsAdvancedScreen(appUiState: AppUiState, viewModel: AppViewModel) { - val context = LocalContext.current + val context = LocalContext.current - val activity = context as? MainActivity + val activity = context as? MainActivity - // Secure screen due to sensitive information - DisposableEffect(Unit) { - activity?.window?.setFlags( - WindowManager.LayoutParams.FLAG_SECURE, - WindowManager.LayoutParams.FLAG_SECURE, - ) - onDispose { - activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) - } - } + // Secure screen due to sensitive information + DisposableEffect(Unit) { + activity + ?.window + ?.setFlags( + WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.FLAG_SECURE, + ) + onDispose { activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) } + } - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(top = 24.dp) - .padding(horizontal = 24.dp), - ) { - SurfaceSelectionGroupButton( - listOf( - RemoteControlItem(appUiState, viewModel), - ), - ) - } + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), + modifier = + Modifier.fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(top = 24.dp) + .padding(horizontal = 24.dp), + ) { + SurfaceSelectionGroupButton(listOf(RemoteControlItem(appUiState, viewModel))) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/advanced/components/RemoteControlItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/advanced/components/RemoteControlItem.kt index 65531df4..91a2d281 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/advanced/components/RemoteControlItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/advanced/components/RemoteControlItem.kt @@ -21,39 +21,40 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable fun RemoteControlItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem { - val clipboardManager = LocalClipboardManager.current + val clipboardManager = LocalClipboardManager.current - return SelectionItem( - leadingIcon = Icons.Filled.SmartToy, - trailing = { - ScaledSwitch( - checked = uiState.appState.isRemoteControlEnabled, - onClick = { viewModel.handleEvent(AppEvent.ToggleRemoteControl) }, - ) - }, - description = { - uiState.appState.remoteKey?.let { key -> - AnimatedVisibility(visible = uiState.appState.isRemoteControlEnabled) { - Text( - text = stringResource(R.string.remote_key_template, key), - style = MaterialTheme.typography.bodySmall.copy( - color = MaterialTheme.colorScheme.outline, - ), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.clickable { - clipboardManager.setText(AnnotatedString(key)) - }, - ) - } - } - }, - title = { - Text( - text = stringResource(R.string.enable_remote_app_control), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - onClick = { viewModel.handleEvent(AppEvent.ToggleRemoteControl) }, - ) + return SelectionItem( + leadingIcon = Icons.Filled.SmartToy, + trailing = { + ScaledSwitch( + checked = uiState.appState.isRemoteControlEnabled, + onClick = { viewModel.handleEvent(AppEvent.ToggleRemoteControl) }, + ) + }, + description = { + uiState.appState.remoteKey?.let { key -> + AnimatedVisibility(visible = uiState.appState.isRemoteControlEnabled) { + Text( + text = stringResource(R.string.remote_key_template, key), + style = + MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.outline + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = + Modifier.clickable { clipboardManager.setText(AnnotatedString(key)) }, + ) + } + } + }, + title = { + Text( + text = stringResource(R.string.enable_remote_app_control), + style = + MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + onClick = { viewModel.handleEvent(AppEvent.ToggleRemoteControl) }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/AppearanceScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/AppearanceScreen.kt index 1e7e4183..5b3f1bec 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/AppearanceScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/AppearanceScreen.kt @@ -15,22 +15,13 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.compon @Composable fun AppearanceScreen() { - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), - modifier = Modifier - .fillMaxSize() - .padding(top = 24.dp) - .padding(horizontal = 24.dp), - ) { - SurfaceSelectionGroupButton( - items = listOf(LanguageItem()), - ) - SurfaceSelectionGroupButton( - items = listOf(NotificationsItem()), - ) - SurfaceSelectionGroupButton( - items = listOf(DisplayThemeItem()), - ) - } + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), + modifier = Modifier.fillMaxSize().padding(top = 24.dp).padding(horizontal = 24.dp), + ) { + SurfaceSelectionGroupButton(items = listOf(LanguageItem())) + SurfaceSelectionGroupButton(items = listOf(NotificationsItem())) + SurfaceSelectionGroupButton(items = listOf(DisplayThemeItem())) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/components/DisplayItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/components/DisplayItem.kt index e1cde42f..1913a76d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/components/DisplayItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/components/DisplayItem.kt @@ -14,16 +14,17 @@ import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavControll @Composable fun DisplayThemeItem(): SelectionItem { - val navController = LocalNavController.current - return SelectionItem( - leadingIcon = Icons.Outlined.Contrast, - title = { - Text( - text = stringResource(R.string.display_theme), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - trailing = { ForwardButton { navController.navigate(Route.Display) } }, - onClick = { navController.navigate(Route.Display) }, - ) + val navController = LocalNavController.current + return SelectionItem( + leadingIcon = Icons.Outlined.Contrast, + title = { + Text( + text = stringResource(R.string.display_theme), + style = + MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + trailing = { ForwardButton { navController.navigate(Route.Display) } }, + onClick = { navController.navigate(Route.Display) }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/components/LanguageItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/components/LanguageItem.kt index d44c314b..1abdd1ab 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/components/LanguageItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/components/LanguageItem.kt @@ -14,16 +14,17 @@ import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavControll @Composable fun LanguageItem(): SelectionItem { - val navController = LocalNavController.current - return SelectionItem( - leadingIcon = Icons.Outlined.Translate, - title = { - Text( - text = stringResource(R.string.language), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - trailing = { ForwardButton { navController.navigate(Route.Language) } }, - onClick = { navController.navigate(Route.Language) }, - ) + val navController = LocalNavController.current + return SelectionItem( + leadingIcon = Icons.Outlined.Translate, + title = { + Text( + text = stringResource(R.string.language), + style = + MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + trailing = { ForwardButton { navController.navigate(Route.Language) } }, + onClick = { navController.navigate(Route.Language) }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/components/NotificationItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/components/NotificationItem.kt index 208f4fe9..6ba01887 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/components/NotificationItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/components/NotificationItem.kt @@ -14,16 +14,17 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.launchNotificationSet @Composable fun NotificationsItem(): SelectionItem { - val context = LocalContext.current - return SelectionItem( - leadingIcon = Icons.Outlined.Notifications, - title = { - Text( - text = stringResource(R.string.notifications), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - trailing = { ForwardButton { context.launchNotificationSettings() } }, - onClick = { context.launchNotificationSettings() }, - ) + val context = LocalContext.current + return SelectionItem( + leadingIcon = Icons.Outlined.Notifications, + title = { + Text( + text = stringResource(R.string.notifications), + style = + MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + trailing = { ForwardButton { context.launchNotificationSettings() } }, + onClick = { context.launchNotificationSettings() }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/display/DisplayScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/display/DisplayScreen.kt index 63f24539..54cf98d6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/display/DisplayScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/display/DisplayScreen.kt @@ -13,42 +13,35 @@ import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.ui.common.button.IconSurfaceButton import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState import com.zaneschepke.wireguardautotunnel.ui.theme.Theme - import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable fun DisplayScreen(appUiState: AppUiState, viewModel: AppViewModel) { - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), - modifier = - Modifier - .fillMaxSize() - .padding(top = 24.dp) - .padding(horizontal = 24.dp), - ) { - IconSurfaceButton( - title = stringResource(R.string.automatic), - onClick = { - viewModel.handleEvent(AppEvent.SetTheme(Theme.AUTOMATIC)) - }, - selected = appUiState.appState.theme == Theme.AUTOMATIC, - ) - IconSurfaceButton( - title = stringResource(R.string.light), - onClick = { viewModel.handleEvent(AppEvent.SetTheme(Theme.LIGHT)) }, - selected = appUiState.appState.theme == Theme.LIGHT, - ) - IconSurfaceButton( - title = stringResource(R.string.dark), - onClick = { viewModel.handleEvent(AppEvent.SetTheme(Theme.DARK)) }, - selected = appUiState.appState.theme == Theme.DARK, - ) - IconSurfaceButton( - title = stringResource(R.string.dynamic), - onClick = { viewModel.handleEvent(AppEvent.SetTheme(Theme.DYNAMIC)) }, - selected = appUiState.appState.theme == Theme.DYNAMIC, - ) - } + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), + modifier = Modifier.fillMaxSize().padding(top = 24.dp).padding(horizontal = 24.dp), + ) { + IconSurfaceButton( + title = stringResource(R.string.automatic), + onClick = { viewModel.handleEvent(AppEvent.SetTheme(Theme.AUTOMATIC)) }, + selected = appUiState.appState.theme == Theme.AUTOMATIC, + ) + IconSurfaceButton( + title = stringResource(R.string.light), + onClick = { viewModel.handleEvent(AppEvent.SetTheme(Theme.LIGHT)) }, + selected = appUiState.appState.theme == Theme.LIGHT, + ) + IconSurfaceButton( + title = stringResource(R.string.dark), + onClick = { viewModel.handleEvent(AppEvent.SetTheme(Theme.DARK)) }, + selected = appUiState.appState.theme == Theme.DARK, + ) + IconSurfaceButton( + title = stringResource(R.string.dynamic), + onClick = { viewModel.handleEvent(AppEvent.SetTheme(Theme.DYNAMIC)) }, + selected = appUiState.appState.theme == Theme.DYNAMIC, + ) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/language/LanguageScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/language/LanguageScreen.kt index 3a8137eb..1502d206 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/language/LanguageScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/language/LanguageScreen.kt @@ -13,34 +13,31 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.langua import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.components.LanguageItem import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState import com.zaneschepke.wireguardautotunnel.util.LocaleUtil - import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel import java.text.Collator import java.util.* @Composable fun LanguageScreen(appUiState: AppUiState, viewModel: AppViewModel) { - val collator = Collator.getInstance(Locale.getDefault()) + val collator = Collator.getInstance(Locale.getDefault()) - val locales = LocaleUtil.supportedLocales.map { - val tag = it.replace("_", "-") - Locale.forLanguageTag(tag) - } + val locales = + LocaleUtil.supportedLocales.map { + val tag = it.replace("_", "-") + Locale.forLanguageTag(tag) + } - val sortedLocales = remember(locales) { - locales.sortedWith(compareBy(collator) { it.getDisplayName(it) }).toList() - } + val sortedLocales = + remember(locales) { + locales.sortedWith(compareBy(collator) { it.getDisplayName(it) }).toList() + } - LazyColumn( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top, - modifier = Modifier.padding(horizontal = 24.dp), - ) { - item { - AutomaticLanguageItem(appUiState, viewModel) - } - items(sortedLocales, key = { it }) { locale -> - LanguageItem(locale, appUiState, viewModel) - } - } + LazyColumn( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + modifier = Modifier.padding(horizontal = 24.dp), + ) { + item { AutomaticLanguageItem(appUiState, viewModel) } + items(sortedLocales, key = { it }) { locale -> LanguageItem(locale, appUiState, viewModel) } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/language/components/AutomaticLanguageItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/language/components/AutomaticLanguageItem.kt index 77911337..e6947c3f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/language/components/AutomaticLanguageItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/language/components/AutomaticLanguageItem.kt @@ -7,8 +7,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.zaneschepke.wireguardautotunnel.R -import com.zaneschepke.wireguardautotunnel.ui.common.label.SelectedLabel import com.zaneschepke.wireguardautotunnel.ui.common.button.SelectionItemButton +import com.zaneschepke.wireguardautotunnel.ui.common.label.SelectedLabel import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState import com.zaneschepke.wireguardautotunnel.util.LocaleUtil import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel @@ -16,20 +16,20 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable fun AutomaticLanguageItem(appUiState: AppUiState, viewModel: AppViewModel) { - Box(modifier = Modifier.padding(top = 24.dp)) { - SelectionItemButton( - buttonText = stringResource(R.string.automatic), - onClick = { - viewModel.handleEvent(AppEvent.SetLocale(LocaleUtil.OPTION_PHONE_LANGUAGE)) - }, - trailing = { - with(appUiState.appState.locale) { - if (this == LocaleUtil.OPTION_PHONE_LANGUAGE || this == null) { - SelectedLabel() - } - } - }, - ripple = false, - ) - } + Box(modifier = Modifier.padding(top = 24.dp)) { + SelectionItemButton( + buttonText = stringResource(R.string.automatic), + onClick = { + viewModel.handleEvent(AppEvent.SetLocale(LocaleUtil.OPTION_PHONE_LANGUAGE)) + }, + trailing = { + with(appUiState.appState.locale) { + if (this == LocaleUtil.OPTION_PHONE_LANGUAGE || this == null) { + SelectedLabel() + } + } + }, + ripple = false, + ) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/language/components/LanguageItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/language/components/LanguageItem.kt index 9e313845..8db776f1 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/language/components/LanguageItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/language/components/LanguageItem.kt @@ -1,8 +1,8 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.components import androidx.compose.runtime.Composable -import com.zaneschepke.wireguardautotunnel.ui.common.label.SelectedLabel import com.zaneschepke.wireguardautotunnel.ui.common.button.SelectionItemButton +import com.zaneschepke.wireguardautotunnel.ui.common.label.SelectedLabel import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @@ -10,24 +10,24 @@ import java.util.* @Composable fun LanguageItem(locale: Locale, appUiState: AppUiState, viewModel: AppViewModel) { - SelectionItemButton( - buttonText = locale.getDisplayLanguage(locale).replaceFirstChar { - if (it.isLowerCase()) it.titlecase(locale) else it.toString() - } + if (locale.toLanguageTag().contains("-")) { - " (${locale.getDisplayCountry(locale).replaceFirstChar { + SelectionItemButton( + buttonText = + locale.getDisplayLanguage(locale).replaceFirstChar { + if (it.isLowerCase()) it.titlecase(locale) else it.toString() + } + + if (locale.toLanguageTag().contains("-")) { + " (${locale.getDisplayCountry(locale).replaceFirstChar { if (it.isLowerCase()) it.titlecase(locale) else it.toString() }})" - } else { - "" - }, - onClick = { - viewModel.handleEvent(AppEvent.SetLocale(locale.toLanguageTag())) - }, - trailing = { - if (locale.toLanguageTag() == appUiState.appState.locale) { - SelectedLabel() - } - }, - ripple = false, - ) + } else { + "" + }, + onClick = { viewModel.handleEvent(AppEvent.SetLocale(locale.toLanguageTag())) }, + trailing = { + if (locale.toLanguageTag() == appUiState.appState.locale) { + SelectedLabel() + } + }, + ripple = false, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/AlwaysOnVpnItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/AlwaysOnVpnItem.kt index f378fab9..8f379faf 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/AlwaysOnVpnItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/AlwaysOnVpnItem.kt @@ -15,28 +15,26 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable fun AlwaysOnVpnItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem { - return SelectionItem( - leadingIcon = Icons.Outlined.VpnLock, - trailing = { - ScaledSwitch( - enabled = !( - ( - uiState.appSettings.isTunnelOnWifiEnabled || - uiState.appSettings.isTunnelOnEthernetEnabled || - uiState.appSettings.isTunnelOnMobileDataEnabled - ) && - uiState.appSettings.isAutoTunnelEnabled - ), - checked = uiState.appSettings.isAlwaysOnVpnEnabled, - onClick = { viewModel.handleEvent(AppEvent.ToggleAlwaysOn) }, - ) - }, - title = { - Text( - text = stringResource(R.string.always_on_vpn_support), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - onClick = { viewModel.handleEvent(AppEvent.ToggleAlwaysOn) }, - ) + return SelectionItem( + leadingIcon = Icons.Outlined.VpnLock, + trailing = { + ScaledSwitch( + enabled = + !((uiState.appSettings.isTunnelOnWifiEnabled || + uiState.appSettings.isTunnelOnEthernetEnabled || + uiState.appSettings.isTunnelOnMobileDataEnabled) && + uiState.appSettings.isAutoTunnelEnabled), + checked = uiState.appSettings.isAlwaysOnVpnEnabled, + onClick = { viewModel.handleEvent(AppEvent.ToggleAlwaysOn) }, + ) + }, + title = { + Text( + text = stringResource(R.string.always_on_vpn_support), + style = + MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + onClick = { viewModel.handleEvent(AppEvent.ToggleAlwaysOn) }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/AppShortcutItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/AppShortcutItem.kt index d42caf40..e5f65c3d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/AppShortcutItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/AppShortcutItem.kt @@ -15,20 +15,21 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable fun AppShortcutsItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem { - return SelectionItem( - leadingIcon = Icons.Filled.AppShortcut, - trailing = { - ScaledSwitch( - checked = uiState.appSettings.isShortcutsEnabled, - onClick = { viewModel.handleEvent(AppEvent.ToggleAppShortcuts) }, - ) - }, - title = { - Text( - text = stringResource(R.string.enabled_app_shortcuts), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - onClick = { viewModel.handleEvent(AppEvent.ToggleAppShortcuts) }, - ) + return SelectionItem( + leadingIcon = Icons.Filled.AppShortcut, + trailing = { + ScaledSwitch( + checked = uiState.appSettings.isShortcutsEnabled, + onClick = { viewModel.handleEvent(AppEvent.ToggleAppShortcuts) }, + ) + }, + title = { + Text( + text = stringResource(R.string.enabled_app_shortcuts), + style = + MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + onClick = { viewModel.handleEvent(AppEvent.ToggleAppShortcuts) }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/AppearanceItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/AppearanceItem.kt index 1f6b8703..3cb046fc 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/AppearanceItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/AppearanceItem.kt @@ -14,16 +14,17 @@ import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavControll @Composable fun AppearanceItem(): SelectionItem { - val navController = LocalNavController.current - return SelectionItem( - leadingIcon = Icons.AutoMirrored.Outlined.ViewQuilt, - title = { - Text( - text = stringResource(R.string.appearance), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - trailing = { ForwardButton { navController.navigate(Route.Appearance) } }, - onClick = { navController.navigate(Route.Appearance) }, - ) + val navController = LocalNavController.current + return SelectionItem( + leadingIcon = Icons.AutoMirrored.Outlined.ViewQuilt, + title = { + Text( + text = stringResource(R.string.appearance), + style = + MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + trailing = { ForwardButton { navController.navigate(Route.Appearance) } }, + onClick = { navController.navigate(Route.Appearance) }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/AuthorizationPromptWrapper.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/AuthorizationPromptWrapper.kt index 0ba0f704..e6ebd62c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/AuthorizationPromptWrapper.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/AuthorizationPromptWrapper.kt @@ -9,20 +9,30 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable -fun AuthorizationPromptWrapper(onSuccess: () -> Unit, onDismiss: () -> Unit, viewModel: AppViewModel) { - val context = LocalContext.current +fun AuthorizationPromptWrapper( + onSuccess: () -> Unit, + onDismiss: () -> Unit, + viewModel: AppViewModel, +) { + val context = LocalContext.current - AuthorizationPrompt( - onSuccess = { - onSuccess() - }, - onError = { _ -> - onDismiss() - viewModel.handleEvent(AppEvent.ShowMessage(StringValue.StringResource(R.string.error_authentication_failed))) - }, - onFailure = { - onDismiss() - viewModel.handleEvent(AppEvent.ShowMessage(StringValue.StringResource(R.string.error_authorization_failed))) - }, - ) + AuthorizationPrompt( + onSuccess = { onSuccess() }, + onError = { _ -> + onDismiss() + viewModel.handleEvent( + AppEvent.ShowMessage( + StringValue.StringResource(R.string.error_authentication_failed) + ) + ) + }, + onFailure = { + onDismiss() + viewModel.handleEvent( + AppEvent.ShowMessage( + StringValue.StringResource(R.string.error_authorization_failed) + ) + ) + }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/BackgroundLocationDialog.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/BackgroundLocationDialog.kt index fdd97306..66b99584 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/BackgroundLocationDialog.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/BackgroundLocationDialog.kt @@ -15,35 +15,39 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.launchAppSettings @Composable fun BackgroundLocationDialog(show: Boolean, onDismiss: () -> Unit, onAttest: () -> Unit) { - val context = LocalContext.current - if (show) { - val alwaysOnDescription = buildAnnotatedString { - append(stringResource(R.string.background_location_message)) - append(" ") - pushStringAnnotation(tag = "appSettings", annotation = "") - withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) { - append(stringResource(id = R.string.app_settings)) - } - pop() - append(" ") - append(stringResource(R.string.background_location_message2)) - append(".") - } - InfoDialog( - onDismiss = { onDismiss() }, - onAttest = { onDismiss() }, - title = { Text(text = stringResource(R.string.vpn_denied_dialog_title)) }, - body = { - ClickableText( - text = alwaysOnDescription, - style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.outline), - ) { - alwaysOnDescription.getStringAnnotations(tag = "appSettings", it, it).firstOrNull()?.let { - context.launchAppSettings() - } - } - }, - confirmText = { Text(text = stringResource(R.string.okay)) }, - ) - } + val context = LocalContext.current + if (show) { + val alwaysOnDescription = buildAnnotatedString { + append(stringResource(R.string.background_location_message)) + append(" ") + pushStringAnnotation(tag = "appSettings", annotation = "") + withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) { + append(stringResource(id = R.string.app_settings)) + } + pop() + append(" ") + append(stringResource(R.string.background_location_message2)) + append(".") + } + InfoDialog( + onDismiss = { onDismiss() }, + onAttest = { onDismiss() }, + title = { Text(text = stringResource(R.string.vpn_denied_dialog_title)) }, + body = { + ClickableText( + text = alwaysOnDescription, + style = + MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.outline + ), + ) { + alwaysOnDescription + .getStringAnnotations(tag = "appSettings", it, it) + .firstOrNull() + ?.let { context.launchAppSettings() } + } + }, + confirmText = { Text(text = stringResource(R.string.okay)) }, + ) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/ExportTunnelsBottomSheet.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/ExportTunnelsBottomSheet.kt index 356e21c0..c4ca2343 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/ExportTunnelsBottomSheet.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/ExportTunnelsBottomSheet.kt @@ -28,77 +28,77 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @OptIn(ExperimentalMaterial3Api::class) @Composable fun ExportTunnelsBottomSheet(viewModel: AppViewModel, isRunningOnTv: Boolean) { - var showAuthPrompt by remember { mutableStateOf(false) } - var isAuthorized by remember { mutableStateOf(false) } - var exportType by remember { mutableStateOf(ConfigType.WG) } + var showAuthPrompt by remember { mutableStateOf(false) } + var isAuthorized by remember { mutableStateOf(false) } + var exportType by remember { mutableStateOf(ConfigType.WG) } - fun handleExport() { - viewModel.handleEvent(AppEvent.ToggleBottomSheet) - viewModel.handleEvent(AppEvent.ExportTunnels(exportType)) - } + fun handleExport() { + viewModel.handleEvent(AppEvent.ToggleBottomSheet) + viewModel.handleEvent(AppEvent.ExportTunnels(exportType)) + } - if (showAuthPrompt) { - AuthorizationPromptWrapper( - onDismiss = { showAuthPrompt = false }, - onSuccess = { - showAuthPrompt = false - isAuthorized = true - handleExport() - }, - viewModel = viewModel, - ) - } + if (showAuthPrompt) { + AuthorizationPromptWrapper( + onDismiss = { showAuthPrompt = false }, + onSuccess = { + showAuthPrompt = false + isAuthorized = true + handleExport() + }, + viewModel = viewModel, + ) + } - ModalBottomSheet( - containerColor = MaterialTheme.colorScheme.surface, - onDismissRequest = { viewModel.handleEvent(AppEvent.ToggleBottomSheet) }, - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - exportType = ConfigType.AMNEZIA - if (!isAuthorized && !isRunningOnTv) { - showAuthPrompt = true - return@clickable - } - handleExport() - } - .padding(10.dp), - ) { - Icon( - Icons.Filled.FolderZip, - contentDescription = stringResource(R.string.export_tunnels_amnezia), - modifier = Modifier.padding(10.dp), - ) - Text( - stringResource(R.string.export_tunnels_amnezia), - modifier = Modifier.padding(10.dp), - ) - } - HorizontalDivider() - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - exportType = ConfigType.WG - if (!isAuthorized && !isRunningOnTv) { - showAuthPrompt = true - return@clickable - } - handleExport() - } - .padding(10.dp), - ) { - Icon( - Icons.Filled.FolderZip, - contentDescription = stringResource(R.string.export_tunnels_wireguard), - modifier = Modifier.padding(10.dp), - ) - Text( - stringResource(R.string.export_tunnels_wireguard), - modifier = Modifier.padding(10.dp), - ) - } - } + ModalBottomSheet( + containerColor = MaterialTheme.colorScheme.surface, + onDismissRequest = { viewModel.handleEvent(AppEvent.ToggleBottomSheet) }, + ) { + Row( + modifier = + Modifier.fillMaxWidth() + .clickable { + exportType = ConfigType.AMNEZIA + if (!isAuthorized && !isRunningOnTv) { + showAuthPrompt = true + return@clickable + } + handleExport() + } + .padding(10.dp) + ) { + Icon( + Icons.Filled.FolderZip, + contentDescription = stringResource(R.string.export_tunnels_amnezia), + modifier = Modifier.padding(10.dp), + ) + Text( + stringResource(R.string.export_tunnels_amnezia), + modifier = Modifier.padding(10.dp), + ) + } + HorizontalDivider() + Row( + modifier = + Modifier.fillMaxWidth() + .clickable { + exportType = ConfigType.WG + if (!isAuthorized && !isRunningOnTv) { + showAuthPrompt = true + return@clickable + } + handleExport() + } + .padding(10.dp) + ) { + Icon( + Icons.Filled.FolderZip, + contentDescription = stringResource(R.string.export_tunnels_wireguard), + modifier = Modifier.padding(10.dp), + ) + Text( + stringResource(R.string.export_tunnels_wireguard), + modifier = Modifier.padding(10.dp), + ) + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/KernelModeItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/KernelModeItem.kt index 37630316..25d8d7e1 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/KernelModeItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/KernelModeItem.kt @@ -15,31 +15,31 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable fun KernelModeItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem { - return SelectionItem( - leadingIcon = Icons.Outlined.Code, - title = { - Text( - text = stringResource(R.string.kernel), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - description = { - Text( - text = stringResource(R.string.use_kernel), - style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline), - ) - }, - trailing = { - ScaledSwitch( - checked = uiState.appSettings.isKernelEnabled, - enabled = !( - uiState.appSettings.isAutoTunnelEnabled || - uiState.appSettings.isAlwaysOnVpnEnabled || - uiState.activeTunnels.isNotEmpty() - ), - onClick = { viewModel.handleEvent(AppEvent.ToggleKernelMode) }, - ) - }, - onClick = { viewModel.handleEvent(AppEvent.ToggleKernelMode) }, - ) + return SelectionItem( + leadingIcon = Icons.Outlined.Code, + title = { + Text( + text = stringResource(R.string.kernel), + style = + MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + description = { + Text( + text = stringResource(R.string.use_kernel), + style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline), + ) + }, + trailing = { + ScaledSwitch( + checked = uiState.appSettings.isKernelEnabled, + enabled = + !(uiState.appSettings.isAutoTunnelEnabled || + uiState.appSettings.isAlwaysOnVpnEnabled || + uiState.activeTunnels.isNotEmpty()), + onClick = { viewModel.handleEvent(AppEvent.ToggleKernelMode) }, + ) + }, + onClick = { viewModel.handleEvent(AppEvent.ToggleKernelMode) }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/KillSwitchitem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/KillSwitchitem.kt index 072d464a..caa80ae3 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/KillSwitchitem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/KillSwitchitem.kt @@ -14,16 +14,17 @@ import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavControll @Composable fun KillSwitchItem(): SelectionItem { - val navController = LocalNavController.current - return SelectionItem( - leadingIcon = Icons.Outlined.VpnKeyOff, - title = { - Text( - text = stringResource(R.string.kill_switch_options), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - trailing = { ForwardButton { navController.navigate(Route.KillSwitch) } }, - onClick = { navController.navigate(Route.KillSwitch) }, - ) + val navController = LocalNavController.current + return SelectionItem( + leadingIcon = Icons.Outlined.VpnKeyOff, + title = { + Text( + text = stringResource(R.string.kill_switch_options), + style = + MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + trailing = { ForwardButton { navController.navigate(Route.KillSwitch) } }, + onClick = { navController.navigate(Route.KillSwitch) }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/LearnMoreLinkLabel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/LearnMoreLinkLabel.kt index 4724e8d8..205c3522 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/LearnMoreLinkLabel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/LearnMoreLinkLabel.kt @@ -13,32 +13,26 @@ import com.zaneschepke.wireguardautotunnel.R @Composable fun LearnMoreLinkLabel(onClick: (url: String) -> Unit, url: String) { - // TODO update link when docs are fully updated - val gettingStarted = - buildAnnotatedString { - pushStringAnnotation( - tag = "details", - annotation = url, - ) - withStyle( - style = SpanStyle(color = MaterialTheme.colorScheme.primary), - ) { - append(stringResource(id = R.string.learn_more)) - } - pop() - } - ClickableText( - text = gettingStarted, - style = - MaterialTheme.typography.bodySmall.copy( - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Start, - fontStyle = FontStyle.Italic, - ), - ) { - gettingStarted.getStringAnnotations(tag = "details", it, it) - .firstOrNull()?.let { annotation -> - onClick(annotation.item) - } - } + // TODO update link when docs are fully updated + val gettingStarted = buildAnnotatedString { + pushStringAnnotation(tag = "details", annotation = url) + withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) { + append(stringResource(id = R.string.learn_more)) + } + pop() + } + ClickableText( + text = gettingStarted, + style = + MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Start, + fontStyle = FontStyle.Italic, + ), + ) { + gettingStarted.getStringAnnotations(tag = "details", it, it).firstOrNull()?.let { annotation + -> + onClick(annotation.item) + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/LocalLoggingItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/LocalLoggingItem.kt index 771c7038..2ae20f26 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/LocalLoggingItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/LocalLoggingItem.kt @@ -13,16 +13,16 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable fun LocalLoggingItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem { - return SelectionItem( - leadingIcon = Icons.Outlined.ViewHeadline, - title = { SelectionItemLabel(R.string.local_logging) }, - description = { SelectionItemLabel(R.string.enable_local_logging, isDescription = true) }, - trailing = { - ScaledSwitch( - checked = uiState.appState.isLocalLogsEnabled, - onClick = { viewModel.handleEvent(AppEvent.ToggleLocalLogging) }, - ) - }, - onClick = { viewModel.handleEvent(AppEvent.ToggleLocalLogging) }, - ) + return SelectionItem( + leadingIcon = Icons.Outlined.ViewHeadline, + title = { SelectionItemLabel(R.string.local_logging) }, + description = { SelectionItemLabel(R.string.enable_local_logging, isDescription = true) }, + trailing = { + ScaledSwitch( + checked = uiState.appState.isLocalLogsEnabled, + onClick = { viewModel.handleEvent(AppEvent.ToggleLocalLogging) }, + ) + }, + onClick = { viewModel.handleEvent(AppEvent.ToggleLocalLogging) }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/LocationServicesDialog.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/LocationServicesDialog.kt index e41c9304..36dc4b28 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/LocationServicesDialog.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/LocationServicesDialog.kt @@ -9,26 +9,26 @@ import com.zaneschepke.wireguardautotunnel.R @Composable fun LocationServicesDialog(show: Boolean, onDismiss: () -> Unit, onAttest: () -> Unit) { - if (show) { - AlertDialog( - onDismissRequest = { onDismiss() }, - confirmButton = { - TextButton( - onClick = { - onDismiss() - onAttest() - }, - ) { - Text(text = stringResource(R.string.okay)) - } - }, - dismissButton = { - TextButton(onClick = { onDismiss() }) { - Text(text = stringResource(R.string.cancel)) - } - }, - title = { Text(text = stringResource(R.string.location_services_not_detected)) }, - text = { Text(text = stringResource(R.string.location_services_missing_message)) }, - ) - } + if (show) { + AlertDialog( + onDismissRequest = { onDismiss() }, + confirmButton = { + TextButton( + onClick = { + onDismiss() + onAttest() + } + ) { + Text(text = stringResource(R.string.okay)) + } + }, + dismissButton = { + TextButton(onClick = { onDismiss() }) { + Text(text = stringResource(R.string.cancel)) + } + }, + title = { Text(text = stringResource(R.string.location_services_not_detected)) }, + text = { Text(text = stringResource(R.string.location_services_missing_message)) }, + ) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/PinLockItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/PinLockItem.kt index 9823aafd..3e05a024 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/PinLockItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/PinLockItem.kt @@ -19,32 +19,33 @@ import xyz.teamgravity.pin_lock_compose.PinManager @Composable fun PinLockItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem { - val navController = LocalNavController.current - val context = LocalContext.current + val navController = LocalNavController.current + val context = LocalContext.current - fun onPinLockToggle() { - if (uiState.appState.isPinLockEnabled) { - viewModel.handleEvent(AppEvent.TogglePinLock) - } else { - PinManager.initialize(context) - navController.navigate(Route.Lock) - } - } + fun onPinLockToggle() { + if (uiState.appState.isPinLockEnabled) { + viewModel.handleEvent(AppEvent.TogglePinLock) + } else { + PinManager.initialize(context) + navController.navigate(Route.Lock) + } + } - return SelectionItem( - leadingIcon = Icons.Outlined.Pin, - title = { - Text( - text = stringResource(R.string.enable_app_lock), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - trailing = { - ScaledSwitch( - checked = uiState.appState.isPinLockEnabled, - onClick = { onPinLockToggle() }, - ) - }, - onClick = { onPinLockToggle() }, - ) + return SelectionItem( + leadingIcon = Icons.Outlined.Pin, + title = { + Text( + text = stringResource(R.string.enable_app_lock), + style = + MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + trailing = { + ScaledSwitch( + checked = uiState.appState.isPinLockEnabled, + onClick = { onPinLockToggle() }, + ) + }, + onClick = { onPinLockToggle() }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/ReadLogsItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/ReadLogsItem.kt index f1b81978..b6bc7976 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/ReadLogsItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/ReadLogsItem.kt @@ -12,11 +12,11 @@ import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavControll @Composable fun ReadLogsItem(): SelectionItem { - val navController = LocalNavController.current - return SelectionItem( - leadingIcon = Icons.Filled.ViewTimeline, - title = { SelectionItemLabel(R.string.read_logs) }, - trailing = { ForwardButton { navController.navigate(Route.Logs) } }, - onClick = { navController.navigate(Route.Logs) }, - ) + val navController = LocalNavController.current + return SelectionItem( + leadingIcon = Icons.Filled.ViewTimeline, + title = { SelectionItemLabel(R.string.read_logs) }, + trailing = { ForwardButton { navController.navigate(Route.Logs) } }, + onClick = { navController.navigate(Route.Logs) }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/RestartAtBootItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/RestartAtBootItem.kt index 71cf332b..9abf1b19 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/RestartAtBootItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/RestartAtBootItem.kt @@ -15,20 +15,21 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable fun RestartAtBootItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem { - return SelectionItem( - leadingIcon = Icons.Outlined.Restore, - trailing = { - ScaledSwitch( - checked = uiState.appSettings.isRestoreOnBootEnabled, - onClick = { viewModel.handleEvent(AppEvent.ToggleRestartAtBoot) }, - ) - }, - title = { - Text( - text = stringResource(R.string.restart_at_boot), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - onClick = { viewModel.handleEvent(AppEvent.ToggleRestartAtBoot) }, - ) + return SelectionItem( + leadingIcon = Icons.Outlined.Restore, + trailing = { + ScaledSwitch( + checked = uiState.appSettings.isRestoreOnBootEnabled, + onClick = { viewModel.handleEvent(AppEvent.ToggleRestartAtBoot) }, + ) + }, + title = { + Text( + text = stringResource(R.string.restart_at_boot), + style = + MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + onClick = { viewModel.handleEvent(AppEvent.ToggleRestartAtBoot) }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/disclosure/LocationDisclosureScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/disclosure/LocationDisclosureScreen.kt index 79a081dc..18f4b302 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/disclosure/LocationDisclosureScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/disclosure/LocationDisclosureScreen.kt @@ -17,31 +17,24 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.disclosure.compon import com.zaneschepke.wireguardautotunnel.ui.screens.settings.disclosure.components.SkipItem import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState import com.zaneschepke.wireguardautotunnel.util.extensions.goFromRoot - import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel @Composable fun LocationDisclosureScreen(appUiState: AppUiState, viewModel: AppViewModel) { - val navController = LocalNavController.current + val navController = LocalNavController.current - LaunchedEffect(Unit, appUiState) { - if (appUiState.appState.isLocationDisclosureShown) navController.goFromRoot(Route.AutoTunnel) - } + LaunchedEffect(Unit, appUiState) { + if (appUiState.appState.isLocationDisclosureShown) + navController.goFromRoot(Route.AutoTunnel) + } - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(18.dp, Alignment.Top), - modifier = Modifier - .fillMaxSize() - .padding(top = 18.dp) - .padding(horizontal = 24.dp), - ) { - LocationDisclosureHeader() - SurfaceSelectionGroupButton( - items = listOf(AppSettingsItem(viewModel)), - ) - SurfaceSelectionGroupButton( - items = listOf(SkipItem(viewModel)), - ) - } + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(18.dp, Alignment.Top), + modifier = Modifier.fillMaxSize().padding(top = 18.dp).padding(horizontal = 24.dp), + ) { + LocationDisclosureHeader() + SurfaceSelectionGroupButton(items = listOf(AppSettingsItem(viewModel))) + SurfaceSelectionGroupButton(items = listOf(SkipItem(viewModel))) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/disclosure/components/AppSettingsItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/disclosure/components/AppSettingsItem.kt index aa93915b..b78422ee 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/disclosure/components/AppSettingsItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/disclosure/components/AppSettingsItem.kt @@ -16,26 +16,26 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable fun AppSettingsItem(viewModel: AppViewModel): SelectionItem { - val context = LocalContext.current - return SelectionItem( - leadingIcon = Icons.Outlined.LocationOn, - title = { - Text( - text = stringResource(R.string.launch_app_settings), - style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - trailing = { - ForwardButton { - context.launchAppSettings().also { - viewModel.handleEvent(AppEvent.SetLocationDisclosureShown) - } - } - }, - onClick = { - context.launchAppSettings().also { - viewModel.handleEvent(AppEvent.SetLocationDisclosureShown) - } - }, - ) + val context = LocalContext.current + return SelectionItem( + leadingIcon = Icons.Outlined.LocationOn, + title = { + Text( + text = stringResource(R.string.launch_app_settings), + style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + trailing = { + ForwardButton { + context.launchAppSettings().also { + viewModel.handleEvent(AppEvent.SetLocationDisclosureShown) + } + } + }, + onClick = { + context.launchAppSettings().also { + viewModel.handleEvent(AppEvent.SetLocationDisclosureShown) + } + }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/disclosure/components/LocationDisclosureHeader.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/disclosure/components/LocationDisclosureHeader.kt index dbbd0140..46e23a10 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/disclosure/components/LocationDisclosureHeader.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/disclosure/components/LocationDisclosureHeader.kt @@ -16,20 +16,18 @@ import com.zaneschepke.wireguardautotunnel.R @Composable fun LocationDisclosureHeader() { - val icon = Icons.Rounded.PermScanWifi - Icon( - imageVector = icon, - contentDescription = icon.name, - modifier = Modifier - .padding(30.dp) - .size(128.dp), - ) - Text( - text = stringResource(R.string.prominent_background_location_title), - style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold), - ) - Text( - text = stringResource(R.string.prominent_background_location_message), - style = MaterialTheme.typography.bodyLarge, - ) + val icon = Icons.Rounded.PermScanWifi + Icon( + imageVector = icon, + contentDescription = icon.name, + modifier = Modifier.padding(30.dp).size(128.dp), + ) + Text( + text = stringResource(R.string.prominent_background_location_title), + style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold), + ) + Text( + text = stringResource(R.string.prominent_background_location_message), + style = MaterialTheme.typography.bodyLarge, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/disclosure/components/SkipItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/disclosure/components/SkipItem.kt index be71ea7c..12120a81 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/disclosure/components/SkipItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/disclosure/components/SkipItem.kt @@ -12,16 +12,14 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable fun SkipItem(viewModel: AppViewModel): SelectionItem { - return SelectionItem( - title = { - Text( - text = stringResource(R.string.skip), - style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - trailing = { - ForwardButton { viewModel.handleEvent(AppEvent.SetLocationDisclosureShown) } - }, - onClick = { viewModel.handleEvent(AppEvent.SetLocationDisclosureShown) }, - ) + return SelectionItem( + title = { + Text( + text = stringResource(R.string.skip), + style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + trailing = { ForwardButton { viewModel.handleEvent(AppEvent.SetLocationDisclosureShown) } }, + onClick = { viewModel.handleEvent(AppEvent.SetLocationDisclosureShown) }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/killswitch/KillSwitchScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/killswitch/KillSwitchScreen.kt index c244aa0f..b0158695 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/killswitch/KillSwitchScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/killswitch/KillSwitchScreen.kt @@ -13,36 +13,35 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelec import com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.components.VpnKillSwitchItem import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv - import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable fun KillSwitchScreen(uiState: AppUiState, viewModel: AppViewModel) { - val context = LocalContext.current + val context = LocalContext.current - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), - modifier = Modifier - .fillMaxSize() - .padding(top = 24.dp) - .padding(horizontal = 24.dp), - ) { - if (!context.isRunningOnTv()) { - SurfaceSelectionGroupButton( - items = listOf(NativeKillSwitchItem()), - ) - } - SurfaceSelectionGroupButton( - items = buildList { - if (!uiState.appSettings.isKernelEnabled) { - add(VpnKillSwitchItem(uiState) { viewModel.handleEvent(AppEvent.ToggleVpnKillSwitch) }) - if (uiState.appSettings.isVpnKillSwitchEnabled) { - add(LanTrafficItem(uiState, viewModel)) - } - } - }, - ) - } + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), + modifier = Modifier.fillMaxSize().padding(top = 24.dp).padding(horizontal = 24.dp), + ) { + if (!context.isRunningOnTv()) { + SurfaceSelectionGroupButton(items = listOf(NativeKillSwitchItem())) + } + SurfaceSelectionGroupButton( + items = + buildList { + if (!uiState.appSettings.isKernelEnabled) { + add( + VpnKillSwitchItem(uiState) { + viewModel.handleEvent(AppEvent.ToggleVpnKillSwitch) + } + ) + if (uiState.appSettings.isVpnKillSwitchEnabled) { + add(LanTrafficItem(uiState, viewModel)) + } + } + } + ) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/killswitch/components/LanTrafficItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/killswitch/components/LanTrafficItem.kt index 50f1ac29..bc9df090 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/killswitch/components/LanTrafficItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/killswitch/components/LanTrafficItem.kt @@ -15,26 +15,27 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable fun LanTrafficItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem { - return SelectionItem( - leadingIcon = Icons.Outlined.Lan, - title = { - Text( - text = stringResource(R.string.allow_lan_traffic), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - description = { - Text( - text = stringResource(R.string.bypass_lan_for_kill_switch), - style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline), - ) - }, - trailing = { - ScaledSwitch( - checked = uiState.appSettings.isLanOnKillSwitchEnabled, - onClick = { viewModel.handleEvent(AppEvent.ToggleLanOnKillSwitch) }, - ) - }, - onClick = { viewModel.handleEvent(AppEvent.ToggleLanOnKillSwitch) }, - ) + return SelectionItem( + leadingIcon = Icons.Outlined.Lan, + title = { + Text( + text = stringResource(R.string.allow_lan_traffic), + style = + MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + description = { + Text( + text = stringResource(R.string.bypass_lan_for_kill_switch), + style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline), + ) + }, + trailing = { + ScaledSwitch( + checked = uiState.appSettings.isLanOnKillSwitchEnabled, + onClick = { viewModel.handleEvent(AppEvent.ToggleLanOnKillSwitch) }, + ) + }, + onClick = { viewModel.handleEvent(AppEvent.ToggleLanOnKillSwitch) }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/killswitch/components/NativeKillSwitchItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/killswitch/components/NativeKillSwitchItem.kt index 04921f1b..3f6a1aa9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/killswitch/components/NativeKillSwitchItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/killswitch/components/NativeKillSwitchItem.kt @@ -14,16 +14,17 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.launchVpnSettings @Composable fun NativeKillSwitchItem(): SelectionItem { - val context = LocalContext.current - return SelectionItem( - leadingIcon = Icons.Outlined.AdminPanelSettings, - title = { - Text( - text = stringResource(R.string.native_kill_switch), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - trailing = { ForwardButton { context.launchVpnSettings() } }, - onClick = { context.launchVpnSettings() }, - ) + val context = LocalContext.current + return SelectionItem( + leadingIcon = Icons.Outlined.AdminPanelSettings, + title = { + Text( + text = stringResource(R.string.native_kill_switch), + style = + MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + trailing = { ForwardButton { context.launchVpnSettings() } }, + onClick = { context.launchVpnSettings() }, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/killswitch/components/VpnKillSwitchItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/killswitch/components/VpnKillSwitchItem.kt index e6018cc9..e7a59fee 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/killswitch/components/VpnKillSwitchItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/killswitch/components/VpnKillSwitchItem.kt @@ -13,20 +13,21 @@ import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState @Composable fun VpnKillSwitchItem(uiState: AppUiState, toggleVpnSwitch: () -> Unit): SelectionItem { - return SelectionItem( - leadingIcon = Icons.Outlined.VpnKey, - title = { - Text( - text = stringResource(R.string.vpn_kill_switch), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - trailing = { - ScaledSwitch( - checked = uiState.appSettings.isVpnKillSwitchEnabled, - onClick = { toggleVpnSwitch() }, - ) - }, - onClick = { toggleVpnSwitch() }, - ) + return SelectionItem( + leadingIcon = Icons.Outlined.VpnKey, + title = { + Text( + text = stringResource(R.string.vpn_kill_switch), + style = + MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + trailing = { + ScaledSwitch( + checked = uiState.appSettings.isVpnKillSwitchEnabled, + onClick = { toggleVpnSwitch() }, + ) + }, + onClick = { toggleVpnSwitch() }, + ) } 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 ae5a4312..e4ac7ad8 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 @@ -20,19 +20,20 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.Version @Composable fun SupportScreen() { - val context = LocalContext.current + val context = LocalContext.current - Column( - modifier = Modifier - .fillMaxSize() - .padding(top = 24.dp).padding(horizontal = 24.dp) - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), - ) { - GroupLabel(stringResource(R.string.thank_you)) - GeneralSupportOptions(context) - ContactSupportOptions(context) - VersionLabel() - } + Column( + modifier = + Modifier.fillMaxSize() + .padding(top = 24.dp) + .padding(horizontal = 24.dp) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), + ) { + GroupLabel(stringResource(R.string.thank_you)) + GeneralSupportOptions(context) + ContactSupportOptions(context) + VersionLabel() + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/components/ContactSupportOptions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/components/ContactSupportOptions.kt index c7c8f7d7..03968420 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/components/ContactSupportOptions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/components/ContactSupportOptions.kt @@ -18,46 +18,65 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl @Composable fun ContactSupportOptions(context: android.content.Context) { - SurfaceSelectionGroupButton( - items = buildList { - addAll( - listOf( - SelectionItem( - leadingIcon = ImageVector.vectorResource(R.drawable.matrix), - title = { SelectionItemLabel(R.string.join_matrix) }, - trailing = { ForwardButton { context.openWebUrl(context.getString(R.string.matrix_url)) } }, - onClick = { context.openWebUrl(context.getString(R.string.matrix_url)) }, - ), - SelectionItem( - leadingIcon = ImageVector.vectorResource(R.drawable.telegram), - title = { SelectionItemLabel(R.string.join_telegram) }, - trailing = { ForwardButton { context.openWebUrl(context.getString(R.string.telegram_url)) } }, - onClick = { context.openWebUrl(context.getString(R.string.telegram_url)) }, - ), - SelectionItem( - leadingIcon = ImageVector.vectorResource(R.drawable.github), - title = { SelectionItemLabel(R.string.open_issue) }, - trailing = { ForwardButton { context.openWebUrl(context.getString(R.string.github_url)) } }, - onClick = { context.openWebUrl(context.getString(R.string.github_url)) }, - ), - SelectionItem( - leadingIcon = Icons.Filled.Mail, - title = { SelectionItemLabel(R.string.email_description) }, - trailing = { ForwardButton { context.launchSupportEmail() } }, - onClick = { context.launchSupportEmail() }, - ), - ), - ) - if (BuildConfig.FLAVOR == Constants.FDROID_FLAVOR) { - add( - SelectionItem( - leadingIcon = Icons.Filled.Favorite, - title = { SelectionItemLabel(R.string.donate) }, - trailing = { ForwardButton { context.openWebUrl(context.getString(R.string.donate_url)) } }, - onClick = { context.openWebUrl(context.getString(R.string.donate_url)) }, - ), - ) - } - }, - ) + SurfaceSelectionGroupButton( + items = + buildList { + addAll( + listOf( + SelectionItem( + leadingIcon = ImageVector.vectorResource(R.drawable.matrix), + title = { SelectionItemLabel(R.string.join_matrix) }, + trailing = { + ForwardButton { + context.openWebUrl(context.getString(R.string.matrix_url)) + } + }, + onClick = { context.openWebUrl(context.getString(R.string.matrix_url)) }, + ), + SelectionItem( + leadingIcon = ImageVector.vectorResource(R.drawable.telegram), + title = { SelectionItemLabel(R.string.join_telegram) }, + trailing = { + ForwardButton { + context.openWebUrl(context.getString(R.string.telegram_url)) + } + }, + onClick = { + context.openWebUrl(context.getString(R.string.telegram_url)) + }, + ), + SelectionItem( + leadingIcon = ImageVector.vectorResource(R.drawable.github), + title = { SelectionItemLabel(R.string.open_issue) }, + trailing = { + ForwardButton { + context.openWebUrl(context.getString(R.string.github_url)) + } + }, + onClick = { context.openWebUrl(context.getString(R.string.github_url)) }, + ), + SelectionItem( + leadingIcon = Icons.Filled.Mail, + title = { SelectionItemLabel(R.string.email_description) }, + trailing = { ForwardButton { context.launchSupportEmail() } }, + onClick = { context.launchSupportEmail() }, + ), + ) + ) + if (BuildConfig.FLAVOR == Constants.FDROID_FLAVOR) { + add( + SelectionItem( + leadingIcon = Icons.Filled.Favorite, + title = { SelectionItemLabel(R.string.donate) }, + trailing = { + ForwardButton { + context.openWebUrl(context.getString(R.string.donate_url)) + } + }, + onClick = { context.openWebUrl(context.getString(R.string.donate_url)) }, + ) + ) + } + } + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/components/GeneralSupportOptions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/components/GeneralSupportOptions.kt index a51595c3..652c96d9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/components/GeneralSupportOptions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/components/GeneralSupportOptions.kt @@ -13,24 +13,35 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl @Composable fun GeneralSupportOptions(context: android.content.Context) { - SurfaceSelectionGroupButton( - items = buildList { - add( - SelectionItem( - leadingIcon = Icons.Filled.Book, - title = { SelectionItemLabel(R.string.docs_description) }, - trailing = { ForwardButton { context.openWebUrl(context.getString(R.string.docs_url)) } }, - onClick = { context.openWebUrl(context.getString(R.string.docs_url)) }, - ), - ) - add( - SelectionItem( - leadingIcon = Icons.Filled.Policy, - title = { SelectionItemLabel(R.string.privacy_policy) }, - trailing = { ForwardButton { context.openWebUrl(context.getString(R.string.privacy_policy_url)) } }, - onClick = { context.openWebUrl(context.getString(R.string.privacy_policy_url)) }, - ), - ) - }, - ) + SurfaceSelectionGroupButton( + items = + buildList { + add( + SelectionItem( + leadingIcon = Icons.Filled.Book, + title = { SelectionItemLabel(R.string.docs_description) }, + trailing = { + ForwardButton { + context.openWebUrl(context.getString(R.string.docs_url)) + } + }, + onClick = { context.openWebUrl(context.getString(R.string.docs_url)) }, + ) + ) + add( + SelectionItem( + leadingIcon = Icons.Filled.Policy, + title = { SelectionItemLabel(R.string.privacy_policy) }, + trailing = { + ForwardButton { + context.openWebUrl(context.getString(R.string.privacy_policy_url)) + } + }, + onClick = { + context.openWebUrl(context.getString(R.string.privacy_policy_url)) + }, + ) + ) + } + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/components/VersionLabel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/components/VersionLabel.kt index 4fbd6613..4b069917 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/components/VersionLabel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/components/VersionLabel.kt @@ -17,23 +17,22 @@ import com.zaneschepke.wireguardautotunnel.util.Constants @Composable fun VersionLabel() { - val clipboardManager = LocalClipboardManager.current - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Start, - ) { - val versionText = if (BuildConfig.BUILD_TYPE == Constants.RELEASE) { - BuildConfig.VERSION_NAME - } else { - "${BuildConfig.VERSION_NAME}-${BuildConfig.BUILD_TYPE}" - } - Text( - "${stringResource(R.string.version)}: $versionText", - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.outline, - modifier = Modifier.clickable { - clipboardManager.setText(AnnotatedString(BuildConfig.VERSION_NAME)) - }, - ) - } + val clipboardManager = LocalClipboardManager.current + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start) { + val versionText = + if (BuildConfig.BUILD_TYPE == Constants.RELEASE) { + BuildConfig.VERSION_NAME + } else { + "${BuildConfig.VERSION_NAME}-${BuildConfig.BUILD_TYPE}" + } + Text( + "${stringResource(R.string.version)}: $versionText", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.outline, + modifier = + Modifier.clickable { + clipboardManager.setText(AnnotatedString(BuildConfig.VERSION_NAME)) + }, + ) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsScreen.kt index 8fb9dd86..943f526b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsScreen.kt @@ -21,47 +21,50 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel @Composable fun LogsScreen(appViewState: AppViewState, viewModel: AppViewModel) { - val logs by viewModel.logs.collectAsStateWithLifecycle() + val logs by viewModel.logs.collectAsStateWithLifecycle() - val lazyColumnListState = rememberLazyListState() - var isAutoScrolling by remember { mutableStateOf(true) } - var lastScrollPosition by remember { mutableIntStateOf(0) } + val lazyColumnListState = rememberLazyListState() + var isAutoScrolling by remember { mutableStateOf(true) } + var lastScrollPosition by remember { mutableIntStateOf(0) } - LaunchedEffect(isAutoScrolling) { - if (isAutoScrolling) { - lazyColumnListState.animateScrollToItem(logs.size) - } - } + LaunchedEffect(isAutoScrolling) { + if (isAutoScrolling) { + lazyColumnListState.animateScrollToItem(logs.size) + } + } - LaunchedEffect(logs.size) { - if (isAutoScrolling) { - lazyColumnListState.animateScrollToItem(logs.size) - } - } + LaunchedEffect(logs.size) { + if (isAutoScrolling) { + lazyColumnListState.animateScrollToItem(logs.size) + } + } - LaunchedEffect(lazyColumnListState) { - snapshotFlow { lazyColumnListState.firstVisibleItemIndex } - .collect { currentScrollPosition -> - if (currentScrollPosition < lastScrollPosition && isAutoScrolling) { - isAutoScrolling = false - } - val visible = lazyColumnListState.layoutInfo.visibleItemsInfo - if (visible.isNotEmpty() && visible.last().index == lazyColumnListState.layoutInfo.totalItemsCount - 1 && !isAutoScrolling) { - isAutoScrolling = true - } - lastScrollPosition = currentScrollPosition - } - } + LaunchedEffect(lazyColumnListState) { + snapshotFlow { lazyColumnListState.firstVisibleItemIndex } + .collect { currentScrollPosition -> + if (currentScrollPosition < lastScrollPosition && isAutoScrolling) { + isAutoScrolling = false + } + val visible = lazyColumnListState.layoutInfo.visibleItemsInfo + if ( + visible.isNotEmpty() && + visible.last().index == + lazyColumnListState.layoutInfo.totalItemsCount - 1 && + !isAutoScrolling + ) { + isAutoScrolling = true + } + lastScrollPosition = currentScrollPosition + } + } - if (appViewState.showBottomSheet) { - LogsBottomSheet(viewModel) - } + if (appViewState.showBottomSheet) { + LogsBottomSheet(viewModel) + } - LogList( - logs = logs, - lazyColumnListState = lazyColumnListState, - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 24.dp), - ) + LogList( + logs = logs, + lazyColumnListState = lazyColumnListState, + modifier = Modifier.fillMaxSize().padding(horizontal = 24.dp), + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/components/LogItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/components/LogItem.kt index efc0ed04..5eb8769f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/components/LogItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/components/LogItem.kt @@ -21,37 +21,24 @@ import com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel @Composable fun LogItem(log: LogMessage) { - val clipboardManager = LocalClipboardManager.current - val fontSize = 10.sp + val clipboardManager = LocalClipboardManager.current + val fontSize = 10.sp - Row( - horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start), - verticalAlignment = Alignment.Top, - modifier = Modifier - .fillMaxSize() - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - onClick = { - clipboardManager.setText(AnnotatedString(log.toString())) - }, - ), - ) { - Text( - text = log.tag, - modifier = Modifier.fillMaxSize(0.3f), - fontSize = fontSize, - ) - LogTypeLabel(color = Color(log.level.color())) { - Text( - text = log.level.signifier, - textAlign = TextAlign.Center, - fontSize = fontSize, - ) - } - Text( - text = "${log.message} - ${log.time}", - fontSize = fontSize, - ) - } + Row( + horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start), + verticalAlignment = Alignment.Top, + modifier = + Modifier.fillMaxSize() + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { clipboardManager.setText(AnnotatedString(log.toString())) }, + ), + ) { + Text(text = log.tag, modifier = Modifier.fillMaxSize(0.3f), fontSize = fontSize) + LogTypeLabel(color = Color(log.level.color())) { + Text(text = log.level.signifier, textAlign = TextAlign.Center, fontSize = fontSize) + } + Text(text = "${log.message} - ${log.time}", fontSize = fontSize) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/components/LogList.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/components/LogList.kt index e1b6b4e4..19b48611 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/components/LogList.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/components/LogList.kt @@ -11,19 +11,21 @@ import androidx.compose.ui.unit.dp import com.zaneschepke.logcatter.model.LogMessage @Composable -fun LogList(logs: List, lazyColumnListState: LazyListState, modifier: Modifier = Modifier) { - LazyColumn( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top), - state = lazyColumnListState, - modifier = modifier, - ) { - itemsIndexed( - items = logs, - key = { index, _ -> index }, - contentType = { _, _ -> null }, - ) { _, log -> - LogItem(log = log) - } - } +fun LogList( + logs: List, + lazyColumnListState: LazyListState, + modifier: Modifier = Modifier, +) { + LazyColumn( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top), + state = lazyColumnListState, + modifier = modifier, + ) { + itemsIndexed(items = logs, key = { index, _ -> index }, contentType = { _, _ -> null }) { + _, + log -> + LogItem(log = log) + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/components/LogsBottomSheet.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/components/LogsBottomSheet.kt index ae862152..1da21719 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/components/LogsBottomSheet.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/components/LogsBottomSheet.kt @@ -24,48 +24,42 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @OptIn(ExperimentalMaterial3Api::class) @Composable fun LogsBottomSheet(viewModel: AppViewModel) { - ModalBottomSheet( - containerColor = MaterialTheme.colorScheme.surface, - onDismissRequest = { viewModel.handleEvent(AppEvent.ToggleBottomSheet) }, - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - viewModel.handleEvent(AppEvent.ToggleBottomSheet) - viewModel.handleEvent(AppEvent.ExportLogs) - } - .padding(10.dp), - ) { - Icon( - imageVector = Icons.Filled.FolderZip, - contentDescription = stringResource(R.string.export_logs), - modifier = Modifier.padding(10.dp), - ) - Text( - text = stringResource(R.string.export_logs), - modifier = Modifier.padding(10.dp), - ) - } - HorizontalDivider() - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - viewModel.handleEvent(AppEvent.ToggleBottomSheet) - viewModel.handleEvent(AppEvent.DeleteLogs) - } - .padding(10.dp), - ) { - Icon( - imageVector = Icons.Filled.Delete, - contentDescription = stringResource(R.string.delete_logs), - modifier = Modifier.padding(10.dp), - ) - Text( - text = stringResource(R.string.delete_logs), - modifier = Modifier.padding(10.dp), - ) - } - } + ModalBottomSheet( + containerColor = MaterialTheme.colorScheme.surface, + onDismissRequest = { viewModel.handleEvent(AppEvent.ToggleBottomSheet) }, + ) { + Row( + modifier = + Modifier.fillMaxWidth() + .clickable { + viewModel.handleEvent(AppEvent.ToggleBottomSheet) + viewModel.handleEvent(AppEvent.ExportLogs) + } + .padding(10.dp) + ) { + Icon( + imageVector = Icons.Filled.FolderZip, + contentDescription = stringResource(R.string.export_logs), + modifier = Modifier.padding(10.dp), + ) + Text(text = stringResource(R.string.export_logs), modifier = Modifier.padding(10.dp)) + } + HorizontalDivider() + Row( + modifier = + Modifier.fillMaxWidth() + .clickable { + viewModel.handleEvent(AppEvent.ToggleBottomSheet) + viewModel.handleEvent(AppEvent.DeleteLogs) + } + .padding(10.dp) + ) { + Icon( + imageVector = Icons.Filled.Delete, + contentDescription = stringResource(R.string.delete_logs), + modifier = Modifier.padding(10.dp), + ) + Text(text = stringResource(R.string.delete_logs), modifier = Modifier.padding(10.dp)) + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/AppUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/AppUiState.kt index d4e83184..95a6fca9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/AppUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/AppUiState.kt @@ -8,13 +8,13 @@ import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState data class AppUiState( - val appSettings: AppSettings = AppSettings(), - val tunnels: List = emptyList(), - val activeTunnels: Map = emptyMap(), - val appState: AppState = GeneralState().toAppState(), - val isAutoTunnelActive: Boolean = false, - val appConfigurationChange: Boolean = false, - val isAppLoaded: Boolean = false, - val selectedTunnel: TunnelConf? = null, - val networkStatus: NetworkStatus? = null, + val appSettings: AppSettings = AppSettings(), + val tunnels: List = emptyList(), + val activeTunnels: Map = emptyMap(), + val appState: AppState = GeneralState().toAppState(), + val isAutoTunnelActive: Boolean = false, + val appConfigurationChange: Boolean = false, + val isAppLoaded: Boolean = false, + val selectedTunnel: TunnelConf? = null, + val networkStatus: NetworkStatus? = null, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/AppViewState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/AppViewState.kt index 10d53a58..5f225f41 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/AppViewState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/AppViewState.kt @@ -4,12 +4,12 @@ import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf import com.zaneschepke.wireguardautotunnel.util.StringValue data class AppViewState( - val isConfigChanged: Boolean = false, - val errorMessage: StringValue? = null, - val popBackStack: Boolean = false, - val isAppReady: Boolean = false, - val showBottomSheet: Boolean = false, - val selectedTunnel: TunnelConf? = null, - val requestVpnPermission: Boolean = false, - val requestBatteryPermission: Boolean = false, + val isConfigChanged: Boolean = false, + val errorMessage: StringValue? = null, + val popBackStack: Boolean = false, + val isAppReady: Boolean = false, + val showBottomSheet: Boolean = false, + val selectedTunnel: TunnelConf? = null, + val requestVpnPermission: Boolean = false, + val requestBatteryPermission: Boolean = false, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/ConfigProxy.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/ConfigProxy.kt index 0d65f706..42e8a0f0 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/ConfigProxy.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/ConfigProxy.kt @@ -2,35 +2,38 @@ package com.zaneschepke.wireguardautotunnel.ui.state import org.amnezia.awg.config.Config -data class ConfigProxy( - val peers: List, - val `interface`: InterfaceProxy, -) { +data class ConfigProxy(val peers: List, val `interface`: InterfaceProxy) { - fun hasScripts(): Boolean { - return `interface`.preUp.isNotBlank() || `interface`.preDown.isNotBlank() || - `interface`.postUp.isNotBlank() || `interface`.postDown.isNotBlank() - } + fun hasScripts(): Boolean { + return `interface`.preUp.isNotBlank() || + `interface`.preDown.isNotBlank() || + `interface`.postUp.isNotBlank() || + `interface`.postDown.isNotBlank() + } - fun buildConfigs(): Pair { - return Pair( - com.wireguard.config.Config.Builder().apply { - addPeers(peers.map { it.toWgPeer() }) - setInterface(`interface`.toWgInterface()) - }.build(), - Config.Builder().apply { - addPeers(peers.map { it.toAmPeer() }) - setInterface(`interface`.toAmInterface()) - }.build(), - ) - } + fun buildConfigs(): Pair { + return Pair( + com.wireguard.config.Config.Builder() + .apply { + addPeers(peers.map { it.toWgPeer() }) + setInterface(`interface`.toWgInterface()) + } + .build(), + Config.Builder() + .apply { + addPeers(peers.map { it.toAmPeer() }) + setInterface(`interface`.toAmInterface()) + } + .build(), + ) + } - companion object { - fun from(amConfig: Config): ConfigProxy { - return ConfigProxy( - `interface` = InterfaceProxy.from(amConfig.`interface`), - peers = amConfig.peers.map { PeerProxy.from(it) }, - ) - } - } + companion object { + fun from(amConfig: Config): ConfigProxy { + return ConfigProxy( + `interface` = InterfaceProxy.from(amConfig.`interface`), + peers = amConfig.peers.map { PeerProxy.from(it) }, + ) + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/InterfaceProxy.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/InterfaceProxy.kt index c45acb2e..7ba2e270 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/InterfaceProxy.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/InterfaceProxy.kt @@ -5,219 +5,223 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.joinAndTrim import com.zaneschepke.wireguardautotunnel.util.extensions.toTrimmedList data class InterfaceProxy( - val privateKey: String = "", - val publicKey: String = "", - val addresses: String = "", - val dnsServers: String = "", - val listenPort: String = "", - val mtu: String = "", - val includedApplications: MutableSet = mutableSetOf(), - val excludedApplications: MutableSet = mutableSetOf(), - val junkPacketCount: String = "", - val junkPacketMinSize: String = "", - val junkPacketMaxSize: String = "", - val initPacketJunkSize: String = "", - val responsePacketJunkSize: String = "", - val initPacketMagicHeader: String = "", - val responsePacketMagicHeader: String = "", - val underloadPacketMagicHeader: String = "", - val transportPacketMagicHeader: String = "", - val preUp: String = "", - val postUp: String = "", - val preDown: String = "", - val postDown: String = "", + val privateKey: String = "", + val publicKey: String = "", + val addresses: String = "", + val dnsServers: String = "", + val listenPort: String = "", + val mtu: String = "", + val includedApplications: MutableSet = mutableSetOf(), + val excludedApplications: MutableSet = mutableSetOf(), + val junkPacketCount: String = "", + val junkPacketMinSize: String = "", + val junkPacketMaxSize: String = "", + val initPacketJunkSize: String = "", + val responsePacketJunkSize: String = "", + val initPacketMagicHeader: String = "", + val responsePacketMagicHeader: String = "", + val underloadPacketMagicHeader: String = "", + val transportPacketMagicHeader: String = "", + val preUp: String = "", + val postUp: String = "", + val preDown: String = "", + val postDown: String = "", ) { - fun toWgInterface(): Interface { - return Interface.Builder().apply { - parseAddresses(addresses) - parsePrivateKey(privateKey) - if (dnsServers.isNotBlank()) parseDnsServers(dnsServers) - if (mtu.isNotBlank()) parseMtu(mtu) - if (listenPort.isNotBlank()) parseListenPort(listenPort) - includeApplications(includedApplications) - excludeApplications(excludedApplications) - preUp.toTrimmedList().forEach { parsePreUp(it) } - postUp.toTrimmedList().forEach { parsePostUp(it) } - preDown.toTrimmedList().forEach { parsePreDown(it) } - postDown.toTrimmedList().forEach { parsePostDown(it) } - }.build() - } + fun toWgInterface(): Interface { + return Interface.Builder() + .apply { + parseAddresses(addresses) + parsePrivateKey(privateKey) + if (dnsServers.isNotBlank()) parseDnsServers(dnsServers) + if (mtu.isNotBlank()) parseMtu(mtu) + if (listenPort.isNotBlank()) parseListenPort(listenPort) + includeApplications(includedApplications) + excludeApplications(excludedApplications) + preUp.toTrimmedList().forEach { parsePreUp(it) } + postUp.toTrimmedList().forEach { parsePostUp(it) } + preDown.toTrimmedList().forEach { parsePreDown(it) } + postDown.toTrimmedList().forEach { parsePostDown(it) } + } + .build() + } - fun toAmneziaCompatibilityConfig(): InterfaceProxy { - return copy( - junkPacketCount = "4", - junkPacketMinSize = "40", - junkPacketMaxSize = "70", - initPacketJunkSize = "0", - responsePacketJunkSize = "0", - initPacketMagicHeader = "1", - responsePacketMagicHeader = "2", - underloadPacketMagicHeader = "3", - transportPacketMagicHeader = "4", - ) - } + fun toAmneziaCompatibilityConfig(): InterfaceProxy { + return copy( + junkPacketCount = "4", + junkPacketMinSize = "40", + junkPacketMaxSize = "70", + initPacketJunkSize = "0", + responsePacketJunkSize = "0", + initPacketMagicHeader = "1", + responsePacketMagicHeader = "2", + underloadPacketMagicHeader = "3", + transportPacketMagicHeader = "4", + ) + } - fun resetAmneziaProperties(): InterfaceProxy { - return copy( - junkPacketCount = "", - junkPacketMinSize = "", - junkPacketMaxSize = "", - initPacketJunkSize = "", - responsePacketJunkSize = "", - initPacketMagicHeader = "", - responsePacketMagicHeader = "", - underloadPacketMagicHeader = "", - transportPacketMagicHeader = "", - ) - } + fun resetAmneziaProperties(): InterfaceProxy { + return copy( + junkPacketCount = "", + junkPacketMinSize = "", + junkPacketMaxSize = "", + initPacketJunkSize = "", + responsePacketJunkSize = "", + initPacketMagicHeader = "", + responsePacketMagicHeader = "", + underloadPacketMagicHeader = "", + transportPacketMagicHeader = "", + ) + } - fun isAmneziaCompatibilityModeSet(): Boolean { - return junkPacketCount.toIntOrNull() in 3..<5 && - junkPacketMinSize.toIntOrNull() == 40 && - junkPacketMaxSize.toIntOrNull() == 70 && - with(initPacketJunkSize.toIntOrNull()) { this == 0 || this == null } && - with(responsePacketJunkSize.toIntOrNull()) { this == 0 || this == null } && - initPacketMagicHeader.toLongOrNull() == 1L && - responsePacketMagicHeader.toLongOrNull() == 2L && - underloadPacketMagicHeader.toLongOrNull() == 3L && - transportPacketMagicHeader.toLongOrNull() == 4L - } + fun isAmneziaCompatibilityModeSet(): Boolean { + return junkPacketCount.toIntOrNull() in 3..<5 && + junkPacketMinSize.toIntOrNull() == 40 && + junkPacketMaxSize.toIntOrNull() == 70 && + with(initPacketJunkSize.toIntOrNull()) { this == 0 || this == null } && + with(responsePacketJunkSize.toIntOrNull()) { this == 0 || this == null } && + initPacketMagicHeader.toLongOrNull() == 1L && + responsePacketMagicHeader.toLongOrNull() == 2L && + underloadPacketMagicHeader.toLongOrNull() == 3L && + transportPacketMagicHeader.toLongOrNull() == 4L + } - fun toAmInterface(): org.amnezia.awg.config.Interface { - return org.amnezia.awg.config.Interface.Builder().apply { - parseAddresses(addresses) - parsePrivateKey(privateKey) - if (dnsServers.isNotBlank()) parseDnsServers(dnsServers) - if (mtu.isNotBlank()) parseMtu(mtu) - if (listenPort.isNotBlank()) parseListenPort(listenPort) - includeApplications(includedApplications) - excludeApplications(excludedApplications) - preUp.toTrimmedList().forEach { parsePreUp(it) } - postUp.toTrimmedList().forEach { parsePostUp(it) } - preDown.toTrimmedList().forEach { parsePreDown(it) } - postDown.toTrimmedList().forEach { parsePostDown(it) } - if (junkPacketCount.isNotBlank()) parseJunkPacketCount(junkPacketCount) - if (junkPacketMinSize.isNotBlank()) parseJunkPacketMinSize(junkPacketMinSize) - if (junkPacketMaxSize.isNotBlank()) parseJunkPacketMaxSize(junkPacketMaxSize) - if (initPacketJunkSize.isNotBlank()) parseInitPacketJunkSize(initPacketJunkSize) - if (responsePacketJunkSize.isNotBlank()) parseResponsePacketJunkSize(responsePacketJunkSize) - if (initPacketMagicHeader.isNotBlank()) parseInitPacketMagicHeader(initPacketMagicHeader) - if (responsePacketMagicHeader.isNotBlank()) parseResponsePacketMagicHeader(responsePacketMagicHeader) - if (underloadPacketMagicHeader.isNotBlank()) parseUnderloadPacketMagicHeader(underloadPacketMagicHeader) - if (transportPacketMagicHeader.isNotBlank()) parseTransportPacketMagicHeader(transportPacketMagicHeader) - }.build() - } + fun toAmInterface(): org.amnezia.awg.config.Interface { + return org.amnezia.awg.config.Interface.Builder() + .apply { + parseAddresses(addresses) + parsePrivateKey(privateKey) + if (dnsServers.isNotBlank()) parseDnsServers(dnsServers) + if (mtu.isNotBlank()) parseMtu(mtu) + if (listenPort.isNotBlank()) parseListenPort(listenPort) + includeApplications(includedApplications) + excludeApplications(excludedApplications) + preUp.toTrimmedList().forEach { parsePreUp(it) } + postUp.toTrimmedList().forEach { parsePostUp(it) } + preDown.toTrimmedList().forEach { parsePreDown(it) } + postDown.toTrimmedList().forEach { parsePostDown(it) } + if (junkPacketCount.isNotBlank()) parseJunkPacketCount(junkPacketCount) + if (junkPacketMinSize.isNotBlank()) parseJunkPacketMinSize(junkPacketMinSize) + if (junkPacketMaxSize.isNotBlank()) parseJunkPacketMaxSize(junkPacketMaxSize) + if (initPacketJunkSize.isNotBlank()) parseInitPacketJunkSize(initPacketJunkSize) + if (responsePacketJunkSize.isNotBlank()) + parseResponsePacketJunkSize(responsePacketJunkSize) + if (initPacketMagicHeader.isNotBlank()) + parseInitPacketMagicHeader(initPacketMagicHeader) + if (responsePacketMagicHeader.isNotBlank()) + parseResponsePacketMagicHeader(responsePacketMagicHeader) + if (underloadPacketMagicHeader.isNotBlank()) + parseUnderloadPacketMagicHeader(underloadPacketMagicHeader) + if (transportPacketMagicHeader.isNotBlank()) + parseTransportPacketMagicHeader(transportPacketMagicHeader) + } + .build() + } - companion object { - fun from(i: Interface): InterfaceProxy { - return InterfaceProxy( - publicKey = i.keyPair.publicKey.toBase64().trim(), - privateKey = i.keyPair.privateKey.toBase64().trim(), - addresses = i.addresses.joinToString(", ").trim(), - dnsServers = listOf( - i.dnsServers.joinToString(", ").replace("/", "").trim(), - i.dnsSearchDomains.joinAndTrim(), - ).filter { it.isNotEmpty() }.joinToString(", "), - listenPort = - if (i.listenPort.isPresent) { - i.listenPort.get().toString().trim() - } else { - "" - }, - mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "", - includedApplications = i.includedApplications.toMutableSet(), - excludedApplications = i.excludedApplications.toMutableSet(), - preUp = i.preUp.joinAndTrim(), - postUp = i.postUp.joinAndTrim(), - preDown = i.preDown.joinAndTrim(), - postDown = i.postDown.joinAndTrim(), - ) - } + companion object { + fun from(i: Interface): InterfaceProxy { + return InterfaceProxy( + publicKey = i.keyPair.publicKey.toBase64().trim(), + privateKey = i.keyPair.privateKey.toBase64().trim(), + addresses = i.addresses.joinToString(", ").trim(), + dnsServers = + listOf( + i.dnsServers.joinToString(", ").replace("/", "").trim(), + i.dnsSearchDomains.joinAndTrim(), + ) + .filter { it.isNotEmpty() } + .joinToString(", "), + listenPort = + if (i.listenPort.isPresent) { + i.listenPort.get().toString().trim() + } else { + "" + }, + mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "", + includedApplications = i.includedApplications.toMutableSet(), + excludedApplications = i.excludedApplications.toMutableSet(), + preUp = i.preUp.joinAndTrim(), + postUp = i.postUp.joinAndTrim(), + preDown = i.preDown.joinAndTrim(), + postDown = i.postDown.joinAndTrim(), + ) + } - fun from(i: org.amnezia.awg.config.Interface): InterfaceProxy { - return InterfaceProxy( - publicKey = i.keyPair.publicKey.toBase64().trim(), - privateKey = i.keyPair.privateKey.toBase64().trim(), - addresses = i.addresses.joinToString(", ").trim(), - dnsServers = (i.dnsServers + i.dnsSearchDomains).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 "", - includedApplications = i.includedApplications.toMutableSet(), - excludedApplications = i.excludedApplications.toMutableSet(), - preUp = i.preUp.joinAndTrim(), - postUp = i.postUp.joinAndTrim(), - preDown = i.preDown.joinAndTrim(), - postDown = i.postDown.joinAndTrim(), - junkPacketCount = - if (i.junkPacketCount.isPresent) { - i.junkPacketCount.get() - .toString() - } else { - "" - }, - junkPacketMinSize = - if (i.junkPacketMinSize.isPresent) { - i.junkPacketMinSize.get() - .toString() - } else { - "" - }, - junkPacketMaxSize = - if (i.junkPacketMaxSize.isPresent) { - i.junkPacketMaxSize.get() - .toString() - } else { - "" - }, - initPacketJunkSize = - if (i.initPacketJunkSize.isPresent) { - i.initPacketJunkSize.get() - .toString() - } else { - "" - }, - responsePacketJunkSize = - if (i.responsePacketJunkSize.isPresent) { - i.responsePacketJunkSize.get() - .toString() - } else { - "" - }, - initPacketMagicHeader = - if (i.initPacketMagicHeader.isPresent) { - i.initPacketMagicHeader.get() - .toString() - } else { - "" - }, - responsePacketMagicHeader = - if (i.responsePacketMagicHeader.isPresent) { - i.responsePacketMagicHeader.get() - .toString() - } else { - "" - }, - transportPacketMagicHeader = - if (i.transportPacketMagicHeader.isPresent) { - i.transportPacketMagicHeader.get() - .toString() - } else { - "" - }, - underloadPacketMagicHeader = - if (i.underloadPacketMagicHeader.isPresent) { - i.underloadPacketMagicHeader.get() - .toString() - } else { - "" - }, - ) - } - } + fun from(i: org.amnezia.awg.config.Interface): InterfaceProxy { + return InterfaceProxy( + publicKey = i.keyPair.publicKey.toBase64().trim(), + privateKey = i.keyPair.privateKey.toBase64().trim(), + addresses = i.addresses.joinToString(", ").trim(), + dnsServers = + (i.dnsServers + i.dnsSearchDomains).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 "", + includedApplications = i.includedApplications.toMutableSet(), + excludedApplications = i.excludedApplications.toMutableSet(), + preUp = i.preUp.joinAndTrim(), + postUp = i.postUp.joinAndTrim(), + preDown = i.preDown.joinAndTrim(), + postDown = i.postDown.joinAndTrim(), + junkPacketCount = + if (i.junkPacketCount.isPresent) { + i.junkPacketCount.get().toString() + } else { + "" + }, + junkPacketMinSize = + if (i.junkPacketMinSize.isPresent) { + i.junkPacketMinSize.get().toString() + } else { + "" + }, + junkPacketMaxSize = + if (i.junkPacketMaxSize.isPresent) { + i.junkPacketMaxSize.get().toString() + } else { + "" + }, + initPacketJunkSize = + if (i.initPacketJunkSize.isPresent) { + i.initPacketJunkSize.get().toString() + } else { + "" + }, + responsePacketJunkSize = + if (i.responsePacketJunkSize.isPresent) { + i.responsePacketJunkSize.get().toString() + } else { + "" + }, + initPacketMagicHeader = + if (i.initPacketMagicHeader.isPresent) { + i.initPacketMagicHeader.get().toString() + } else { + "" + }, + responsePacketMagicHeader = + if (i.responsePacketMagicHeader.isPresent) { + i.responsePacketMagicHeader.get().toString() + } else { + "" + }, + transportPacketMagicHeader = + if (i.transportPacketMagicHeader.isPresent) { + i.transportPacketMagicHeader.get().toString() + } else { + "" + }, + underloadPacketMagicHeader = + if (i.underloadPacketMagicHeader.isPresent) { + i.underloadPacketMagicHeader.get().toString() + } else { + "" + }, + ) + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/PeerProxy.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/PeerProxy.kt index c1150395..64d8b707 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/PeerProxy.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/PeerProxy.kt @@ -5,96 +5,97 @@ import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf import com.zaneschepke.wireguardautotunnel.util.extensions.joinAndTrim data class PeerProxy( - val publicKey: String = "", - val preSharedKey: String = "", - val persistentKeepalive: String = "", - val endpoint: String = "", - val allowedIps: String = TunnelConf.ALL_IPS.joinAndTrim(), + val publicKey: String = "", + val preSharedKey: String = "", + val persistentKeepalive: String = "", + val endpoint: String = "", + val allowedIps: String = TunnelConf.ALL_IPS.joinAndTrim(), ) { - fun toWgPeer(): Peer { - return Peer.Builder().apply { - parsePublicKey(publicKey) - if (preSharedKey.isNotBlank()) parsePreSharedKey(preSharedKey) - if (persistentKeepalive.isNotBlank()) parsePersistentKeepalive(persistentKeepalive) - parseEndpoint(endpoint) - parseAllowedIPs(allowedIps) - }.build() - } - fun toAmPeer(): org.amnezia.awg.config.Peer { - return org.amnezia.awg.config.Peer.Builder().apply { - parsePublicKey(publicKey) - if (preSharedKey.isNotBlank()) parsePreSharedKey(preSharedKey) - if (persistentKeepalive.isNotBlank()) parsePersistentKeepalive(persistentKeepalive) - parseEndpoint(endpoint) - parseAllowedIPs(allowedIps) - }.build() - } + fun toWgPeer(): Peer { + return Peer.Builder() + .apply { + parsePublicKey(publicKey) + if (preSharedKey.isNotBlank()) parsePreSharedKey(preSharedKey) + if (persistentKeepalive.isNotBlank()) parsePersistentKeepalive(persistentKeepalive) + parseEndpoint(endpoint) + parseAllowedIPs(allowedIps) + } + .build() + } - fun isLanExcluded(): Boolean { - return this.allowedIps.contains(TunnelConf.LAN_BYPASS_ALLOWED_IPS.joinAndTrim()) - } + fun toAmPeer(): org.amnezia.awg.config.Peer { + return org.amnezia.awg.config.Peer.Builder() + .apply { + parsePublicKey(publicKey) + if (preSharedKey.isNotBlank()) parsePreSharedKey(preSharedKey) + if (persistentKeepalive.isNotBlank()) parsePersistentKeepalive(persistentKeepalive) + parseEndpoint(endpoint) + parseAllowedIPs(allowedIps) + } + .build() + } - fun includeLan(): PeerProxy { - return this.copy( - allowedIps = TunnelConf.ALL_IPS.joinAndTrim(), - ) - } + fun isLanExcluded(): Boolean { + return this.allowedIps.contains(TunnelConf.LAN_BYPASS_ALLOWED_IPS.joinAndTrim()) + } - fun excludeLan(): PeerProxy { - return this.copy( - allowedIps = TunnelConf.LAN_BYPASS_ALLOWED_IPS.joinAndTrim(), - ) - } + fun includeLan(): PeerProxy { + return this.copy(allowedIps = TunnelConf.ALL_IPS.joinAndTrim()) + } - companion object { - 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(), - ) - } + fun excludeLan(): PeerProxy { + return this.copy(allowedIps = TunnelConf.LAN_BYPASS_ALLOWED_IPS.joinAndTrim()) + } - fun from(peer: org.amnezia.awg.config.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(), - ) - } - } + companion object { + 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(), + ) + } + + fun from(peer: org.amnezia.awg.config.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(), + ) + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Color.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Color.kt index 1406b41e..a4a7ff4f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Color.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Color.kt @@ -16,32 +16,34 @@ val Brick = Color(0xFFCE4257) val Straw = Color(0xFFD4C483) sealed class ThemeColors( - val background: Color, - val surface: Color, - val primary: Color, - val secondary: Color, - val onSurface: Color, - val onBackground: Color, - val outline: Color, + val background: Color, + val surface: Color, + val primary: Color, + val secondary: Color, + val onSurface: Color, + val onBackground: Color, + val outline: Color, ) { - data object Light : ThemeColors( - background = LightGrey.copy(alpha = 0.95f), - surface = OffWhite, - primary = Aqua, - secondary = LightGrey, - onSurface = BalticSea, - outline = Plantation.copy(alpha = .75f), - onBackground = BalticSea, - ) + data object Light : + ThemeColors( + background = LightGrey.copy(alpha = 0.95f), + surface = OffWhite, + primary = Aqua, + secondary = LightGrey, + onSurface = BalticSea, + outline = Plantation.copy(alpha = .75f), + onBackground = BalticSea, + ) - data object Dark : ThemeColors( - background = BalticSea, - surface = Shark, - primary = Aqua, - secondary = Plantation, - onSurface = OffWhite, - outline = CoolGray, - onBackground = OffWhite, - ) + data object Dark : + ThemeColors( + background = BalticSea, + surface = Shark, + primary = Aqua, + secondary = Plantation, + onSurface = OffWhite, + outline = CoolGray, + onBackground = OffWhite, + ) } 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 57bffd40..9482359a 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 @@ -17,81 +17,78 @@ import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowCompat private val DarkColorScheme = - darkColorScheme( - primary = ThemeColors.Dark.primary, - surface = ThemeColors.Dark.surface, - background = ThemeColors.Dark.background, - secondary = ThemeColors.Dark.secondary, - onSurface = ThemeColors.Dark.onSurface, - onSecondaryContainer = ThemeColors.Dark.primary, - outline = ThemeColors.Dark.outline, - onBackground = ThemeColors.Dark.onBackground, - ) + darkColorScheme( + primary = ThemeColors.Dark.primary, + surface = ThemeColors.Dark.surface, + background = ThemeColors.Dark.background, + secondary = ThemeColors.Dark.secondary, + onSurface = ThemeColors.Dark.onSurface, + onSecondaryContainer = ThemeColors.Dark.primary, + outline = ThemeColors.Dark.outline, + onBackground = ThemeColors.Dark.onBackground, + ) private val LightColorScheme = - lightColorScheme( - primary = ThemeColors.Light.primary, - surface = ThemeColors.Light.surface, - background = ThemeColors.Light.background, - secondary = ThemeColors.Light.secondary, - onSurface = ThemeColors.Light.onSurface, - onSecondaryContainer = ThemeColors.Light.primary, - outline = ThemeColors.Light.outline, - onBackground = ThemeColors.Light.onBackground, - ) + lightColorScheme( + primary = ThemeColors.Light.primary, + surface = ThemeColors.Light.surface, + background = ThemeColors.Light.background, + secondary = ThemeColors.Light.secondary, + onSurface = ThemeColors.Light.onSurface, + onSecondaryContainer = ThemeColors.Light.primary, + outline = ThemeColors.Light.outline, + onBackground = ThemeColors.Light.onBackground, + ) enum class Theme { - AUTOMATIC, - LIGHT, - DARK, - DYNAMIC, + AUTOMATIC, + LIGHT, + DARK, + DYNAMIC, } @Composable fun WireguardAutoTunnelTheme(theme: Theme = Theme.AUTOMATIC, content: @Composable () -> Unit) { - val context = LocalContext.current - var isDark = isSystemInDarkTheme() - val autoTheme = if (isDark) DarkColorScheme else LightColorScheme - val colorScheme = when (theme) { - Theme.AUTOMATIC -> autoTheme - Theme.DARK -> { - isDark = true - DarkColorScheme - } - Theme.LIGHT -> { - isDark = false - LightColorScheme - } - Theme.DYNAMIC -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - if (isDark) { - dynamicDarkColorScheme(context) - } else { - dynamicLightColorScheme(context) - } - } else { - autoTheme - } - } - } + val context = LocalContext.current + var isDark = isSystemInDarkTheme() + val autoTheme = if (isDark) DarkColorScheme else LightColorScheme + val colorScheme = + when (theme) { + Theme.AUTOMATIC -> autoTheme + Theme.DARK -> { + isDark = true + DarkColorScheme + } + Theme.LIGHT -> { + isDark = false + LightColorScheme + } + Theme.DYNAMIC -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (isDark) { + dynamicDarkColorScheme(context) + } else { + dynamicLightColorScheme(context) + } + } else { + autoTheme + } + } + } - val view = LocalView.current - if (!view.isInEditMode) { - SideEffect { - val window = (view.context as Activity).window - WindowCompat.setDecorFitsSystemWindows(window, false) - window.navigationBarColor = Color.Transparent.toArgb() - window.statusBarColor = Color.Transparent.toArgb() - WindowCompat.getInsetsController(window, window.decorView).apply { - isAppearanceLightStatusBars = !isDark - isAppearanceLightNavigationBars = !isDark - } - } - } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + WindowCompat.setDecorFitsSystemWindows(window, false) + window.navigationBarColor = Color.Transparent.toArgb() + window.statusBarColor = Color.Transparent.toArgb() + WindowCompat.getInsetsController(window, window.decorView).apply { + isAppearanceLightStatusBars = !isDark + isAppearanceLightNavigationBars = !isDark + } + } + } - MaterialTheme( - colorScheme = colorScheme, - typography = Typography, - content = content, - ) + MaterialTheme(colorScheme = colorScheme, typography = Typography, content = content) } 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 20a7b835..d8d17db8 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,53 +10,57 @@ import com.zaneschepke.wireguardautotunnel.R // Set of Material typography styles to start with -val inter = FontFamily( - Font(R.font.inter), -) +val inter = FontFamily(Font(R.font.inter)) val Typography = - Typography( - bodyLarge = TextStyle( - fontFamily = inter, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp, - ), - bodySmall = TextStyle( - fontFamily = inter, - fontWeight = FontWeight.Normal, - fontSize = 12.sp, - lineHeight = 20.sp, - letterSpacing = 1.sp, - color = LightGrey, - ), - bodyMedium = TextStyle( - fontFamily = inter, - fontSize = 14.sp, - lineHeight = 18.sp, - fontWeight = FontWeight(400), - letterSpacing = 0.sp, - ), - labelLarge = TextStyle( - fontFamily = inter, - fontWeight = FontWeight.Normal, - fontSize = 15.sp, - lineHeight = 18.sp, - letterSpacing = 0.sp, - ), - labelMedium = TextStyle( - fontFamily = inter, - fontWeight = FontWeight.SemiBold, - fontSize = 12.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp, - ), - titleMedium = TextStyle( - fontFamily = inter, - fontWeight = FontWeight.Bold, - fontSize = 16.sp, - lineHeight = 21.sp, - letterSpacing = 0.sp, - ), - ) + Typography( + bodyLarge = + TextStyle( + fontFamily = inter, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp, + ), + bodySmall = + TextStyle( + fontFamily = inter, + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + lineHeight = 20.sp, + letterSpacing = 1.sp, + color = LightGrey, + ), + bodyMedium = + TextStyle( + fontFamily = inter, + fontSize = 14.sp, + lineHeight = 18.sp, + fontWeight = FontWeight(400), + letterSpacing = 0.sp, + ), + labelLarge = + TextStyle( + fontFamily = inter, + fontWeight = FontWeight.Normal, + fontSize = 15.sp, + lineHeight = 18.sp, + letterSpacing = 0.sp, + ), + labelMedium = + TextStyle( + fontFamily = inter, + fontWeight = FontWeight.SemiBold, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp, + ), + titleMedium = + TextStyle( + fontFamily = inter, + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + lineHeight = 21.sp, + letterSpacing = 0.sp, + ), + ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt index d8e0f3de..38438dea 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt @@ -2,35 +2,35 @@ package com.zaneschepke.wireguardautotunnel.util object Constants { - const val BASE_PACKAGE = "com.zaneschepke.wireguardautotunnel" + const val BASE_PACKAGE = "com.zaneschepke.wireguardautotunnel" - const val BASE_LOG_FILE_NAME = "wg_tunnel_logs" + const val BASE_LOG_FILE_NAME = "wg_tunnel_logs" - const val MANUAL_TUNNEL_CONFIG_ID = 0 - const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1_000L // 10 minutes + const val MANUAL_TUNNEL_CONFIG_ID = 0 + const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1_000L // 10 minutes - const val CONF_FILE_EXTENSION = ".conf" - const val ZIP_FILE_EXTENSION = ".zip" - const val URI_CONTENT_SCHEME = "content" - private const val TEXT_MIME_TYPE = "text/plain" - private const val ZIP_FILE_MIME_TYPE = "application/zip" - const val ALLOWED_TV_FILE_TYPES = "${TEXT_MIME_TYPE}|${ZIP_FILE_MIME_TYPE}" - const val ALL_FILE_TYPES = "*/*" - const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs" - const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs" - const val VPN_SETTINGS_PACKAGE = "android.net.vpn.SETTINGS" - const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024 + const val CONF_FILE_EXTENSION = ".conf" + const val ZIP_FILE_EXTENSION = ".zip" + const val URI_CONTENT_SCHEME = "content" + private const val TEXT_MIME_TYPE = "text/plain" + private const val ZIP_FILE_MIME_TYPE = "application/zip" + const val ALLOWED_TV_FILE_TYPES = "${TEXT_MIME_TYPE}|${ZIP_FILE_MIME_TYPE}" + const val ALL_FILE_TYPES = "*/*" + const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs" + const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs" + const val VPN_SETTINGS_PACKAGE = "android.net.vpn.SETTINGS" + const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024 - const val SUBSCRIPTION_TIMEOUT = 5_000L + const val SUBSCRIPTION_TIMEOUT = 5_000L - const val DEFAULT_PING_IP = "1.1.1.1" - const val PING_TIMEOUT = 5_000L - const val PING_INTERVAL = 60_000L - const val PING_COOLDOWN = PING_INTERVAL * 60 // one hour + const val DEFAULT_PING_IP = "1.1.1.1" + const val PING_TIMEOUT = 5_000L + const val PING_INTERVAL = 60_000L + const val PING_COOLDOWN = PING_INTERVAL * 60 // one hour - val amProperties = listOf("Jc", "Jmin", "Jmax", "S1", "S2", "H1", "H2", "H3", "H4") - const val QR_CODE_NAME_PROPERTY = "# Name =" + val amProperties = listOf("Jc", "Jmin", "Jmax", "S1", "S2", "H1", "H2", "H3", "H4") + const val QR_CODE_NAME_PROPERTY = "# Name =" - const val FDROID_FLAVOR = "fdroid" - const val RELEASE = "release" + const val FDROID_FLAVOR = "fdroid" + const val RELEASE = "release" } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Exceptions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Exceptions.kt index a083f050..9f4bd470 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Exceptions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Exceptions.kt @@ -1,13 +1,13 @@ package com.zaneschepke.wireguardautotunnel.util object InvalidFileExtensionException : Exception() { - private fun readResolve(): Any = InvalidFileExtensionException + private fun readResolve(): Any = InvalidFileExtensionException } object FileReadException : Exception() { - private fun readResolve(): Any = FileReadException + private fun readResolve(): Any = FileReadException } object ConfigExportException : Exception() { - private fun readResolve(): Any = ConfigExportException + private fun readResolve(): Any = ConfigExportException } 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 115f6343..af1da370 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/FileUtils.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/FileUtils.kt @@ -11,153 +11,154 @@ import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf import com.zaneschepke.wireguardautotunnel.util.extensions.getInputStreamFromUri import com.zaneschepke.wireguardautotunnel.util.extensions.launchShareFile import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.withContext -import org.amnezia.awg.config.Config -import timber.log.Timber import java.io.BufferedOutputStream import java.io.File import java.io.FileOutputStream import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import java.util.zip.ZipOutputStream +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext +import org.amnezia.awg.config.Config +import timber.log.Timber -class FileUtils( - private val context: Context, - private val ioDispatcher: CoroutineDispatcher, -) { +class FileUtils(private val context: Context, private val ioDispatcher: CoroutineDispatcher) { - suspend fun createWgFiles(tunnels: List): List = withContext(ioDispatcher) { - tunnels.map { config -> - val file = File(context.cacheDir, "${config.tunName}-wg.conf") - file.outputStream().use { - it.write(config.wgQuick.toByteArray()) - } - file - } - } + suspend fun createWgFiles(tunnels: List): List = + withContext(ioDispatcher) { + tunnels.map { config -> + val file = File(context.cacheDir, "${config.tunName}-wg.conf") + file.outputStream().use { it.write(config.wgQuick.toByteArray()) } + file + } + } - suspend fun createAmFiles(tunnels: List): List = withContext(ioDispatcher) { - tunnels.filter { it.amQuick != TunnelConfig.AM_QUICK_DEFAULT }.map { config -> - val file = File(context.cacheDir, "${config.tunName}-am.conf") - file.outputStream().use { - it.write(config.amQuick.toByteArray()) - } - file - } - } + suspend fun createAmFiles(tunnels: List): List = + withContext(ioDispatcher) { + tunnels + .filter { it.amQuick != TunnelConfig.AM_QUICK_DEFAULT } + .map { config -> + val file = File(context.cacheDir, "${config.tunName}-am.conf") + file.outputStream().use { it.write(config.amQuick.toByteArray()) } + file + } + } - suspend fun zipAll(zipFile: File, files: List) = withContext(ioDispatcher) { - ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFile))).use { zos -> - files.forEach { file -> - val zipFileName = ( - file.parentFile?.let { parent -> - file.absolutePath.removePrefix(parent.absolutePath) - } ?: file.absolutePath - ).removePrefix("/") - val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}") - zos.putNextEntry(entry) - if (file.isFile) { - file.inputStream().use { - it.copyTo(zos) - } - } - } - } - } + suspend fun zipAll(zipFile: File, files: List) = + withContext(ioDispatcher) { + ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFile))).use { zos -> + files.forEach { file -> + val zipFileName = + (file.parentFile?.let { parent -> + file.absolutePath.removePrefix(parent.absolutePath) + } ?: file.absolutePath) + .removePrefix("/") + val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}") + zos.putNextEntry(entry) + if (file.isFile) { + file.inputStream().use { it.copyTo(zos) } + } + } + } + } - suspend fun createNewShareFile(name: String): File = withContext(ioDispatcher) { - val sharePath = File(context.filesDir, "external_files") - if (sharePath.exists()) sharePath.delete() - sharePath.mkdir() - val file = File("${sharePath.path}/$name") - if (file.exists()) file.delete() - file.createNewFile() - file - } + suspend fun createNewShareFile(name: String): File = + withContext(ioDispatcher) { + val sharePath = File(context.filesDir, "external_files") + if (sharePath.exists()) sharePath.delete() + sharePath.mkdir() + val file = File("${sharePath.path}/$name") + if (file.exists()) file.delete() + file.createNewFile() + file + } - private fun getDisplayNameColumnIndex(cursor: Cursor): Int? { - val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) - if (columnIndex == -1) return null - return columnIndex - } + private fun getDisplayNameColumnIndex(cursor: Cursor): Int? { + val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + if (columnIndex == -1) return null + return columnIndex + } - private fun getDisplayNameByCursor(cursor: Cursor): String? { - val move = cursor.moveToFirst() - if (!move) return null - val index = getDisplayNameColumnIndex(cursor) - if (index == null) return index - return cursor.getString(index) - } + private fun getDisplayNameByCursor(cursor: Cursor): String? { + val move = cursor.moveToFirst() + if (!move) return null + val index = getDisplayNameColumnIndex(cursor) + if (index == null) return index + return cursor.getString(index) + } - private fun isValidUriContentScheme(uri: Uri): Boolean { - return uri.scheme == Constants.URI_CONTENT_SCHEME - } + private fun isValidUriContentScheme(uri: Uri): Boolean { + return uri.scheme == Constants.URI_CONTENT_SCHEME + } - private fun getFileName(uri: Uri): String { - return getFileNameByCursor(uri) ?: NumberUtils.generateRandomTunnelName() - } + private fun getFileName(uri: Uri): String { + return getFileNameByCursor(uri) ?: NumberUtils.generateRandomTunnelName() + } - private fun getNameFromFileName(fileName: String): String { - return fileName.substring(0, fileName.lastIndexOf('.')) - } + 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) { - Timber.e(e) - null - } - } + private fun getFileExtensionFromFileName(fileName: String): String? { + return try { + fileName.substring(fileName.lastIndexOf('.')) + } catch (e: Exception) { + Timber.e(e) + null + } + } - private fun getFileNameByCursor(uri: Uri): String? { - return context.contentResolver.query(uri, null, null, null, null)?.use { - getDisplayNameByCursor(it) - } - } + private fun getFileNameByCursor(uri: Uri): String? { + return context.contentResolver.query(uri, null, null, null, null)?.use { + getDisplayNameByCursor(it) + } + } - suspend fun buildTunnelsFromUri(uri: Uri): List = withContext(ioDispatcher) { - if (!isValidUriContentScheme(uri)) throw InvalidFileExtensionException - val fileName = getFileName(uri) - when (getFileExtensionFromFileName(fileName)) { - Constants.CONF_FILE_EXTENSION -> { - context.getInputStreamFromUri(uri)?.use { inputStream -> - val name = getNameFromFileName(fileName) - val amConf = Config.parse(inputStream) - listOf( - TunnelConf( - tunName = name, - wgQuick = amConf.toWgQuickString(), - amQuick = amConf.toAwgQuickString(true), - ), - ) - } ?: throw FileReadException - } - Constants.ZIP_FILE_EXTENSION -> { - ZipInputStream(context.getInputStreamFromUri(uri)).use { zip -> - generateSequence { zip.nextEntry } - .filterNot { - it.isDirectory || - getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION - } - .map { entry -> - val name = getNameFromFileName(entry.name) - val amConf = Config.parse(zip.bufferedReader()) - TunnelConf( - tunName = name, - wgQuick = amConf.toWgQuickString(), - amQuick = amConf.toAwgQuickString(true), - ) - }.toList() - } - } - else -> throw InvalidFileExtensionException - } - } + suspend fun buildTunnelsFromUri(uri: Uri): List = + withContext(ioDispatcher) { + if (!isValidUriContentScheme(uri)) throw InvalidFileExtensionException + val fileName = getFileName(uri) + when (getFileExtensionFromFileName(fileName)) { + Constants.CONF_FILE_EXTENSION -> { + context.getInputStreamFromUri(uri)?.use { inputStream -> + val name = getNameFromFileName(fileName) + val amConf = Config.parse(inputStream) + listOf( + TunnelConf( + tunName = name, + wgQuick = amConf.toWgQuickString(), + amQuick = amConf.toAwgQuickString(true), + ) + ) + } ?: throw FileReadException + } + Constants.ZIP_FILE_EXTENSION -> { + ZipInputStream(context.getInputStreamFromUri(uri)).use { zip -> + generateSequence { zip.nextEntry } + .filterNot { + it.isDirectory || + getFileExtensionFromFileName(it.name) != + Constants.CONF_FILE_EXTENSION + } + .map { entry -> + val name = getNameFromFileName(entry.name) + val amConf = Config.parse(zip.bufferedReader()) + TunnelConf( + tunName = name, + wgQuick = amConf.toWgQuickString(), + amQuick = amConf.toAwgQuickString(true), + ) + } + .toList() + } + } + else -> throw InvalidFileExtensionException + } + } - fun shareFile(shareFile: File) { - val uri = FileProvider.getUriForFile(context, context.getString(R.string.provider), shareFile) - context.launchShareFile(uri) - } + fun shareFile(shareFile: File) { + val uri = + FileProvider.getUriForFile(context, context.getString(R.string.provider), shareFile) + context.launchShareFile(uri) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/LocaleUtil.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/LocaleUtil.kt index b9e2d3a3..4c3f687f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/LocaleUtil.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/LocaleUtil.kt @@ -5,16 +5,16 @@ import androidx.core.os.LocaleListCompat import com.zaneschepke.wireguardautotunnel.BuildConfig object LocaleUtil { - val supportedLocales: Array = BuildConfig.LANGUAGES - const val OPTION_PHONE_LANGUAGE = "sys_def" + val supportedLocales: Array = BuildConfig.LANGUAGES + const val OPTION_PHONE_LANGUAGE = "sys_def" - fun changeLocale(locale: String) { - if (locale == OPTION_PHONE_LANGUAGE) return resetToSystemLanguage() - val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(locale) - AppCompatDelegate.setApplicationLocales(appLocale) - } + fun changeLocale(locale: String) { + if (locale == OPTION_PHONE_LANGUAGE) return resetToSystemLanguage() + val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(locale) + AppCompatDelegate.setApplicationLocales(appLocale) + } - private fun resetToSystemLanguage() { - AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList()) - } + private fun resetToSystemLanguage() { + AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList()) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/NumberUtils.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/NumberUtils.kt index 248a7f80..0b3eaa6e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/NumberUtils.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/NumberUtils.kt @@ -6,36 +6,36 @@ import java.time.Instant import kotlin.math.pow object NumberUtils { - private const val BYTES_IN_KB = 1024.0 - private val BYTES_IN_MB = BYTES_IN_KB.pow(2.0) - private val keyValidationRegex = """^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=${'$'}""".toRegex() + private const val BYTES_IN_KB = 1024.0 + private val BYTES_IN_MB = BYTES_IN_KB.pow(2.0) + private val keyValidationRegex = """^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=${'$'}""".toRegex() - fun bytesToMB(bytes: Long): BigDecimal { - return bytes.toBigDecimal().divide(BYTES_IN_MB.toBigDecimal()) - } + fun bytesToMB(bytes: Long): BigDecimal { + return bytes.toBigDecimal().divide(BYTES_IN_MB.toBigDecimal()) + } - fun isValidKey(key: String): Boolean { - return key.matches(keyValidationRegex) - } + fun isValidKey(key: String): Boolean { + return key.matches(keyValidationRegex) + } - fun generateRandomTunnelName(): String { - return "tunnel${randomFive()}" - } + fun generateRandomTunnelName(): String { + return "tunnel${randomFive()}" + } - private fun randomFive(): Int { - return (Math.random() * 100000).toInt() - } + private fun randomFive(): Int { + return (Math.random() * 100000).toInt() + } - fun randomThree(): Int { - return (Math.random() * 1000).toInt() - } + fun randomThree(): Int { + return (Math.random() * 1000).toInt() + } - fun getSecondsBetweenTimestampAndNow(epoch: Long): Long? { - return if (epoch != 0L) { - val time = Instant.ofEpochMilli(epoch) - return Duration.between(time, Instant.now()).seconds - } else { - null - } - } + fun getSecondsBetweenTimestampAndNow(epoch: Long): Long? { + return if (epoch != 0L) { + val time = Instant.ofEpochMilli(epoch) + return Duration.between(time, Instant.now()).seconds + } else { + null + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/ReleaseTree.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/ReleaseTree.kt index 03b06136..2cd1fd6d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/ReleaseTree.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/ReleaseTree.kt @@ -3,15 +3,15 @@ package com.zaneschepke.wireguardautotunnel.util import timber.log.Timber class ReleaseTree : Timber.DebugTree() { - override fun d(t: Throwable?) { - return - } + override fun d(t: Throwable?) { + return + } - override fun d(t: Throwable?, message: String?, vararg args: Any?) { - return - } + override fun d(t: Throwable?, message: String?, vararg args: Any?) { + return + } - override fun d(message: String?, vararg args: Any?) { - return - } + override fun d(message: String?, vararg args: Any?) { + return + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/StringValue.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/StringValue.kt index cdca8ebb..61b132d0 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/StringValue.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/StringValue.kt @@ -4,20 +4,17 @@ import android.content.Context import androidx.annotation.StringRes sealed class StringValue { - data class DynamicString(val value: String) : StringValue() + data class DynamicString(val value: String) : StringValue() - data object Empty : StringValue() + data object Empty : StringValue() - class StringResource( - @StringRes val resId: Int, - vararg val args: Any, - ) : StringValue() + class StringResource(@StringRes val resId: Int, vararg val args: Any) : StringValue() - fun asString(context: Context?): String { - return when (this) { - is Empty -> "" - is DynamicString -> value - is StringResource -> context?.getString(resId, *args).orEmpty() - } - } + fun asString(context: Context?): String { + return when (this) { + is Empty -> "" + is DynamicString -> value + is StringResource -> context?.getString(resId, *args).orEmpty() + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/ContextExtensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/ContextExtensions.kt index 08139f15..e2b64f88 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/ContextExtensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/ContextExtensions.kt @@ -29,178 +29,171 @@ private const val BASELINE_DENSITY = 2.625 private const val ANDROID_TV_SIZE_MULTIPLIER = 1.5f fun Context.openWebUrl(url: String): Result { - return kotlin.runCatching { - val webpage: Uri = Uri.parse(url) - val intent = Intent(Intent.ACTION_VIEW, webpage).apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } - startActivity(intent) - }.onFailure { - showToast(R.string.no_browser_detected) - } + return kotlin + .runCatching { + val webpage: Uri = Uri.parse(url) + val intent = + Intent(Intent.ACTION_VIEW, webpage).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + startActivity(intent) + } + .onFailure { showToast(R.string.no_browser_detected) } } fun Context.isBatteryOptimizationsDisabled(): Boolean { - val pm = getSystemService(POWER_SERVICE) as PowerManager - return pm.isIgnoringBatteryOptimizations(packageName) + val pm = getSystemService(POWER_SERVICE) as PowerManager + return pm.isIgnoringBatteryOptimizations(packageName) } val Context.actionBarSize - get() = theme.obtainStyledAttributes(intArrayOf(android.R.attr.actionBarSize)) - .let { attrs -> attrs.getDimension(0, 0F).toInt().also { attrs.recycle() } } + get() = + theme.obtainStyledAttributes(intArrayOf(android.R.attr.actionBarSize)).let { attrs -> + attrs.getDimension(0, 0F).toInt().also { attrs.recycle() } + } fun Context.resizeHeight(dp: Dp): Dp { - val displayMetrics = resources.displayMetrics - val density = displayMetrics.density - val height = (displayMetrics.heightPixels - this.actionBarSize) * - (if (isRunningOnTv()) ANDROID_TV_SIZE_MULTIPLIER else 1f) - val resizeHeightPercentage = - (height.toFloat() / BASELINE_HEIGHT) * (BASELINE_DENSITY.toFloat() / density) - return dp * resizeHeightPercentage + val displayMetrics = resources.displayMetrics + val density = displayMetrics.density + val height = + (displayMetrics.heightPixels - this.actionBarSize) * + (if (isRunningOnTv()) ANDROID_TV_SIZE_MULTIPLIER else 1f) + val resizeHeightPercentage = + (height.toFloat() / BASELINE_HEIGHT) * (BASELINE_DENSITY.toFloat() / density) + return dp * resizeHeightPercentage } fun Context.resizeHeight(textUnit: TextUnit): TextUnit { - val displayMetrics = resources.displayMetrics - val density = displayMetrics.density - val height = (displayMetrics.heightPixels - actionBarSize) * - (if (isRunningOnTv()) ANDROID_TV_SIZE_MULTIPLIER else 1f) - val resizeHeightPercentage = - (height.toFloat() / BASELINE_HEIGHT) * (BASELINE_DENSITY.toFloat() / density) - return textUnit * resizeHeightPercentage * 1.1 + val displayMetrics = resources.displayMetrics + val density = displayMetrics.density + val height = + (displayMetrics.heightPixels - actionBarSize) * + (if (isRunningOnTv()) ANDROID_TV_SIZE_MULTIPLIER else 1f) + val resizeHeightPercentage = + (height.toFloat() / BASELINE_HEIGHT) * (BASELINE_DENSITY.toFloat() / density) + return textUnit * resizeHeightPercentage * 1.1 } fun Context.resizeWidth(dp: Dp): Dp { - val displayMetrics = resources.displayMetrics - val density = displayMetrics.density - val width = displayMetrics.widthPixels - val resizeWidthPercentage = - (width.toFloat() / BASELINE_WIDTH) * (BASELINE_DENSITY.toFloat() / density) - return dp * resizeWidthPercentage + val displayMetrics = resources.displayMetrics + val density = displayMetrics.density + val width = displayMetrics.widthPixels + val resizeWidthPercentage = + (width.toFloat() / BASELINE_WIDTH) * (BASELINE_DENSITY.toFloat() / density) + return dp * resizeWidthPercentage } fun Context.launchNotificationSettings() { - if (isRunningOnTv()) return launchAppSettings() - val settingsIntent: Intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .putExtra(Settings.EXTRA_APP_PACKAGE, packageName) - this.startActivity(settingsIntent) + if (isRunningOnTv()) return launchAppSettings() + val settingsIntent: Intent = + Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtra(Settings.EXTRA_APP_PACKAGE, packageName) + this.startActivity(settingsIntent) } fun Context.launchShareFile(file: Uri) { - val shareIntent = Intent().apply { - action = Intent.ACTION_SEND - type = Constants.ALL_FILE_TYPES - putExtra(Intent.EXTRA_STREAM, file) - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } - val chooserIntent = Intent.createChooser(shareIntent, "").apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } - this.startActivity(chooserIntent) + val shareIntent = + Intent().apply { + action = Intent.ACTION_SEND + type = Constants.ALL_FILE_TYPES + putExtra(Intent.EXTRA_STREAM, file) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + val chooserIntent = + Intent.createChooser(shareIntent, "").apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } + this.startActivity(chooserIntent) } fun Context.isLocationServicesEnabled(): Boolean { - val locationManager = - getSystemService( - Context.LOCATION_SERVICE, - ) as LocationManager - return LocationManagerCompat.isLocationEnabled(locationManager) + val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager + return LocationManagerCompat.isLocationEnabled(locationManager) } fun Context.showToast(resId: Int) { - Toast.makeText( - this, - this.getString(resId), - Toast.LENGTH_LONG, - ).show() + Toast.makeText(this, this.getString(resId), Toast.LENGTH_LONG).show() } fun Context.launchSupportEmail() { - val intent = - Intent(Intent.ACTION_SENDTO).apply { - data = Uri.parse("mailto:") - putExtra(Intent.EXTRA_EMAIL, arrayOf(getString(R.string.my_email))) - putExtra(Intent.EXTRA_SUBJECT, getString(R.string.email_subject)) - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } - if (intent.resolveActivity(packageManager) != null) { - startActivity( - Intent.createChooser(intent, getString(R.string.email_chooser)).apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - }, - ) - } else { - showToast(R.string.no_email_detected) - } + val intent = + Intent(Intent.ACTION_SENDTO).apply { + data = Uri.parse("mailto:") + putExtra(Intent.EXTRA_EMAIL, arrayOf(getString(R.string.my_email))) + putExtra(Intent.EXTRA_SUBJECT, getString(R.string.email_subject)) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + if (intent.resolveActivity(packageManager) != null) { + startActivity( + Intent.createChooser(intent, getString(R.string.email_chooser)).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + ) + } else { + showToast(R.string.no_email_detected) + } } fun Context.isRunningOnTv(): Boolean { - return packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) + return packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) } fun Context.launchVpnSettings(): Result { - return kotlin.runCatching { - val intent = Intent(Constants.VPN_SETTINGS_PACKAGE).apply { - setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } - startActivity(intent) - } + return kotlin.runCatching { + val intent = + Intent(Constants.VPN_SETTINGS_PACKAGE).apply { setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } + startActivity(intent) + } } fun Context.getInputStreamFromUri(uri: Uri): InputStream? { - return this.applicationContext.contentResolver.openInputStream(uri) + return this.applicationContext.contentResolver.openInputStream(uri) } fun Context.launchLocationServicesSettings(): Result { - return kotlin.runCatching { - val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - startActivity(intent) - } + return kotlin.runCatching { + val intent = + Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + startActivity(intent) + } } fun Context.launchSettings(): Result { - return kotlin.runCatching { - val intent = Intent(Settings.ACTION_SETTINGS).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - startActivity(intent) - } + return kotlin.runCatching { + val intent = + Intent(Settings.ACTION_SETTINGS).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK } + startActivity(intent) + } } fun Context.launchAppSettings() { - kotlin.runCatching { - val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { - data = Uri.fromParts("package", packageName, null) - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - startActivity(intent) - } + kotlin.runCatching { + val intent = + Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", packageName, null) + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + startActivity(intent) + } } fun Context.requestTunnelTileServiceStateUpdate() { - TileService.requestListeningState( - this, - ComponentName(this, TunnelControlTile::class.java), - ) + TileService.requestListeningState(this, ComponentName(this, TunnelControlTile::class.java)) } fun Context.requestAutoTunnelTileServiceUpdate() { - TileService.requestListeningState( - this, - ComponentName(this, AutoTunnelControlTile::class.java), - ) + TileService.requestListeningState(this, ComponentName(this, AutoTunnelControlTile::class.java)) } fun Context.getAllInternetCapablePackages(): List { - val permissions = arrayOf(Manifest.permission.INTERNET) - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - packageManager.getPackagesHoldingPermissions( - permissions, - PackageManager.PackageInfoFlags.of(0L), - ) - } else { - packageManager.getPackagesHoldingPermissions(permissions, 0) - } + val permissions = arrayOf(Manifest.permission.INTERNET) + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + packageManager.getPackagesHoldingPermissions( + permissions, + PackageManager.PackageInfoFlags.of(0L), + ) + } else { + packageManager.getPackagesHoldingPermissions(permissions, 0) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/CoroutineExtensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/CoroutineExtensions.kt index 554a1bd4..0d31aecf 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/CoroutineExtensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/CoroutineExtensions.kt @@ -1,6 +1,8 @@ package com.zaneschepke.wireguardautotunnel.util.extensions import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState +import java.time.Duration +import java.util.concurrent.ConcurrentLinkedQueue import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ObsoleteCoroutinesApi @@ -14,52 +16,51 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first import kotlinx.coroutines.selects.whileSelect import timber.log.Timber -import java.time.Duration -import java.util.concurrent.ConcurrentLinkedQueue /** * Chunks based on a time or size threshold. * - * Borrowed from this [Stack Overflow question](https://stackoverflow.com/questions/51022533/kotlin-chunk-sequence-based-on-size-and-time). + * Borrowed from this + * [Stack Overflow question](https://stackoverflow.com/questions/51022533/kotlin-chunk-sequence-based-on-size-and-time). */ @OptIn(ObsoleteCoroutinesApi::class, ExperimentalCoroutinesApi::class) -fun ReceiveChannel.chunked(scope: CoroutineScope, size: Int, time: Duration) = scope.produce> { - while (true) { // this loop goes over each chunk - val chunk = ConcurrentLinkedQueue() // current chunk - val ticker = ticker(time.toMillis()) // time-limit for this chunk - try { - whileSelect { - ticker.onReceive { - false // done with chunk when timer ticks, takes priority over received elements - } - this@chunked.onReceive { - chunk += it - chunk.size < size // continue whileSelect if chunk is not full - } - } - } catch (e: ClosedReceiveChannelException) { - Timber.e(e) - return@produce - } finally { - ticker.cancel() - if (chunk.isNotEmpty()) { - send(chunk.toList()) - } - } - } -} +fun ReceiveChannel.chunked(scope: CoroutineScope, size: Int, time: Duration) = + scope.produce> { + while (true) { // this loop goes over each chunk + val chunk = ConcurrentLinkedQueue() // current chunk + val ticker = ticker(time.toMillis()) // time-limit for this chunk + try { + whileSelect { + ticker.onReceive { + false // done with chunk when timer ticks, takes priority over received + // elements + } + this@chunked.onReceive { + chunk += it + chunk.size < size // continue whileSelect if chunk is not full + } + } + } catch (e: ClosedReceiveChannelException) { + Timber.e(e) + return@produce + } finally { + ticker.cancel() + if (chunk.isNotEmpty()) { + send(chunk.toList()) + } + } + } + } fun Flow>.distinctByKeys(): Flow> { - return distinctUntilChanged { old, new -> old.keys == new.keys } + return distinctUntilChanged { old, new -> old.keys == new.keys } } @ExperimentalCoroutinesApi fun CoroutineScope.asChannel(flow: Flow): ReceiveChannel = produce { - flow.collect { value -> - channel.send(value) - } + flow.collect { value -> channel.send(value) } } suspend fun StateFlow.withFirstState(block: suspend (AppUiState) -> R): R { - return block(first { it.isAppLoaded }) + return block(first { it.isAppLoaded }) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/Extensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/Extensions.kt index d0f0f609..405a48af 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/Extensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/Extensions.kt @@ -7,8 +7,8 @@ import java.math.BigDecimal import java.text.DecimalFormat fun BigDecimal.toThreeDecimalPlaceString(): String { - val df = DecimalFormat("#.###") - return df.format(this) + val df = DecimalFormat("#.###") + return df.format(this) } fun List.update(index: Int, item: T): List = toMutableList().apply { this[index] = item } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/StringExtensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/StringExtensions.kt index 24645289..dfdbb4f6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/StringExtensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/StringExtensions.kt @@ -1,67 +1,63 @@ package com.zaneschepke.wireguardautotunnel.util.extensions -import timber.log.Timber import java.util.regex.Pattern +import timber.log.Timber val hasNumberInParentheses = """^(.+?)\((\d+)\)$""".toRegex() fun String.isValidIpv4orIpv6Address(): Boolean { - val ipv4Pattern = Pattern.compile( - "^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\$", - ) - val ipv6Pattern = Pattern.compile( - "^([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}\$", - ) - return ipv4Pattern.matcher(this).matches() || ipv6Pattern.matcher(this).matches() + val ipv4Pattern = Pattern.compile("^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\$") + val ipv6Pattern = Pattern.compile("^([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}\$") + return ipv4Pattern.matcher(this).matches() || ipv6Pattern.matcher(this).matches() } fun String.hasNumberInParentheses(): Boolean { - return hasNumberInParentheses.matches(this) + return hasNumberInParentheses.matches(this) } // Function to extract name and number fun String.extractNameAndNumber(): Pair? { - val matchResult = hasNumberInParentheses.matchEntire(this) - return matchResult?.let { - Pair(it.groupValues[1], it.groupValues[2].toInt()) - } + val matchResult = hasNumberInParentheses.matchEntire(this) + return matchResult?.let { Pair(it.groupValues[1], it.groupValues[2].toInt()) } } fun List.isMatchingToWildcardList(value: String): Boolean { - val excludeValues = this.filter { it.startsWith("!") }.map { it.removePrefix("!").transformWildcardsToRegex() } - Timber.d("Excluded values: $excludeValues") - val includedValues = this.filter { !it.startsWith("!") }.map { it.transformWildcardsToRegex() } - Timber.d("Included values: $includedValues") - val matches = includedValues.filter { it.matches(value) } - val excludedMatches = excludeValues.filter { it.matches(value) } - Timber.d("Excluded matches: $excludedMatches") - Timber.d("Matches: $matches") - return matches.isNotEmpty() && excludedMatches.isEmpty() + val excludeValues = + this.filter { it.startsWith("!") }.map { it.removePrefix("!").transformWildcardsToRegex() } + Timber.d("Excluded values: $excludeValues") + val includedValues = this.filter { !it.startsWith("!") }.map { it.transformWildcardsToRegex() } + Timber.d("Included values: $includedValues") + val matches = includedValues.filter { it.matches(value) } + val excludedMatches = excludeValues.filter { it.matches(value) } + Timber.d("Excluded matches: $excludedMatches") + Timber.d("Matches: $matches") + return matches.isNotEmpty() && excludedMatches.isEmpty() } fun String.transformWildcardsToRegex(): Regex { - return this.replaceUnescapedChar("*", ".*").replaceUnescapedChar("?", ".").toRegex() + return this.replaceUnescapedChar("*", ".*").replaceUnescapedChar("?", ".").toRegex() } fun String.replaceUnescapedChar(charToReplace: String, replacement: String): String { - val escapedChar = Regex.escape(charToReplace) - val regex = "(? - if (matchResult.range.first == 0 || - this[matchResult.range.first - 1] != '\\' || - (matchResult.range.first > 1 && this[matchResult.range.first - 2] == '\\') - ) { - replacement - } else { - matchResult.value - } - } + val escapedChar = Regex.escape(charToReplace) + val regex = "(? + if ( + matchResult.range.first == 0 || + this[matchResult.range.first - 1] != '\\' || + (matchResult.range.first > 1 && this[matchResult.range.first - 2] == '\\') + ) { + replacement + } else { + matchResult.value + } + } } fun Iterable.joinAndTrim(): String { - return this.joinToString(", ").trim() + return this.joinToString(", ").trim() } fun String.toTrimmedList(): List { - return this.split(",").map { it.trim() }.filter { it.isNotEmpty() } + return this.split(",").map { it.trim() }.filter { it.isNotEmpty() } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt index 0a94dcf4..28fea67c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt @@ -12,129 +12,135 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree import com.zaneschepke.wireguardautotunnel.ui.theme.Straw import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.NumberUtils +import java.net.InetAddress import org.amnezia.awg.backend.Backend import org.amnezia.awg.backend.Tunnel import org.amnezia.awg.config.Config import timber.log.Timber -import java.net.InetAddress fun TunnelStatistics.mapPeerStats(): Map { - return this.getPeers().associateWith { key -> (this.peerStats(key)) } + return this.getPeers().associateWith { key -> (this.peerStats(key)) } } fun TunnelStatistics.PeerStats.latestHandshakeSeconds(): Long? { - return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis) + return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis) } fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus { - // TODO add never connected status after duration - return this.latestHandshakeSeconds().let { - when { - it == null -> HandshakeStatus.NOT_STARTED - it <= HandshakeStatus.STALE_TIME_LIMIT_SEC -> HandshakeStatus.HEALTHY - it > HandshakeStatus.STALE_TIME_LIMIT_SEC -> HandshakeStatus.STALE - else -> { - HandshakeStatus.UNKNOWN - } - } - } + // TODO add never connected status after duration + return this.latestHandshakeSeconds().let { + when { + it == null -> HandshakeStatus.NOT_STARTED + it <= HandshakeStatus.STALE_TIME_LIMIT_SEC -> HandshakeStatus.HEALTHY + it > HandshakeStatus.STALE_TIME_LIMIT_SEC -> HandshakeStatus.STALE + else -> { + HandshakeStatus.UNKNOWN + } + } + } } fun Peer.isReachable(): Boolean { - val host = if (this.endpoint.isPresent) { - this.endpoint.get().host - } else { - Constants.DEFAULT_PING_IP - } - Timber.d("Checking reachability of peer: $host") - val reachable = - InetAddress.getByName(host) - .isReachable(Constants.PING_TIMEOUT.toInt()) - return reachable + val host = + if (this.endpoint.isPresent) { + this.endpoint.get().host + } else { + Constants.DEFAULT_PING_IP + } + Timber.d("Checking reachability of peer: $host") + val reachable = InetAddress.getByName(host).isReachable(Constants.PING_TIMEOUT.toInt()) + return reachable } fun TunnelStatistics?.asColor(): Color { - return this?.mapPeerStats() - ?.map { it.value?.handshakeStatus() } - ?.let { statuses -> - when { - statuses.all { it == HandshakeStatus.HEALTHY } -> SilverTree - statuses.any { it == HandshakeStatus.STALE } -> Straw - statuses.all { it == HandshakeStatus.NOT_STARTED } -> Color.Gray - else -> Color.Gray - } - } ?: Color.Gray + return this?.mapPeerStats() + ?.map { it.value?.handshakeStatus() } + ?.let { statuses -> + when { + statuses.all { it == HandshakeStatus.HEALTHY } -> SilverTree + statuses.any { it == HandshakeStatus.STALE } -> Straw + statuses.all { it == HandshakeStatus.NOT_STARTED } -> Color.Gray + else -> Color.Gray + } + } ?: Color.Gray } fun Config.toWgQuickString(): String { - val amQuick = toAwgQuickString(true) - val lines = amQuick.lines().toMutableList() - val linesIterator = lines.iterator() - while (linesIterator.hasNext()) { - val next = linesIterator.next() - Constants.amProperties.forEach { - if (next.startsWith(it, ignoreCase = true)) { - linesIterator.remove() - } - } - } - return lines.joinToString(System.lineSeparator()) + val amQuick = toAwgQuickString(true) + val lines = amQuick.lines().toMutableList() + val linesIterator = lines.iterator() + while (linesIterator.hasNext()) { + val next = linesIterator.next() + Constants.amProperties.forEach { + if (next.startsWith(it, ignoreCase = true)) { + linesIterator.remove() + } + } + } + return lines.joinToString(System.lineSeparator()) } fun Config.defaultName(): String { - return try { - this.peers[0].endpoint.get().host - } catch (e: Exception) { - Timber.Forest.e(e) - NumberUtils.generateRandomTunnelName() - } + return try { + this.peers[0].endpoint.get().host + } catch (e: Exception) { + Timber.Forest.e(e) + NumberUtils.generateRandomTunnelName() + } } fun Backend.BackendState.asBackendState(): BackendState { - return BackendState.valueOf(this.name) + return BackendState.valueOf(this.name) } fun BackendState.asAmBackendState(): Backend.BackendState { - return Backend.BackendState.valueOf(this.name) + return Backend.BackendState.valueOf(this.name) } fun Tunnel.State.asTunnelState(): TunnelStatus { - return when (this) { - Tunnel.State.DOWN -> TunnelStatus.Down - Tunnel.State.UP -> TunnelStatus.Up - } + return when (this) { + Tunnel.State.DOWN -> TunnelStatus.Down + Tunnel.State.UP -> TunnelStatus.Up + } } fun BackendException.toBackendError(): BackendError { - return when (this.reason) { - BackendException.Reason.VPN_NOT_AUTHORIZED -> BackendError.Unauthorized - BackendException.Reason.DNS_RESOLUTION_FAILURE -> BackendError.DNS - BackendException.Reason.UNKNOWN_KERNEL_MODULE_NAME -> BackendError.KernelModuleName - BackendException.Reason.WG_QUICK_CONFIG_ERROR_CODE -> BackendError.Config - BackendException.Reason.TUNNEL_MISSING_CONFIG -> BackendError.Config - BackendException.Reason.UNABLE_TO_START_VPN -> BackendError.NotAuthorized - BackendException.Reason.TUN_CREATION_ERROR -> BackendError.NotAuthorized - BackendException.Reason.GO_ACTIVATION_ERROR_CODE -> BackendError.Unknown - } + return when (this.reason) { + BackendException.Reason.VPN_NOT_AUTHORIZED -> BackendError.Unauthorized + BackendException.Reason.DNS_RESOLUTION_FAILURE -> BackendError.DNS + BackendException.Reason.UNKNOWN_KERNEL_MODULE_NAME -> BackendError.KernelModuleName + BackendException.Reason.WG_QUICK_CONFIG_ERROR_CODE -> BackendError.Config + BackendException.Reason.TUNNEL_MISSING_CONFIG -> BackendError.Config + BackendException.Reason.UNABLE_TO_START_VPN -> BackendError.NotAuthorized + BackendException.Reason.TUN_CREATION_ERROR -> BackendError.NotAuthorized + BackendException.Reason.GO_ACTIVATION_ERROR_CODE -> BackendError.Unknown + } } fun org.amnezia.awg.backend.BackendException.toBackendError(): BackendError { - return when (this.reason) { - org.amnezia.awg.backend.BackendException.Reason.VPN_NOT_AUTHORIZED -> BackendError.Unauthorized - org.amnezia.awg.backend.BackendException.Reason.DNS_RESOLUTION_FAILURE -> BackendError.DNS - org.amnezia.awg.backend.BackendException.Reason.UNKNOWN_KERNEL_MODULE_NAME -> BackendError.KernelModuleName - org.amnezia.awg.backend.BackendException.Reason.AWG_QUICK_CONFIG_ERROR_CODE -> BackendError.Config - org.amnezia.awg.backend.BackendException.Reason.TUNNEL_MISSING_CONFIG -> BackendError.Config - org.amnezia.awg.backend.BackendException.Reason.UNABLE_TO_START_VPN -> BackendError.NotAuthorized - org.amnezia.awg.backend.BackendException.Reason.TUN_CREATION_ERROR -> BackendError.NotAuthorized - org.amnezia.awg.backend.BackendException.Reason.GO_ACTIVATION_ERROR_CODE -> BackendError.Unknown - org.amnezia.awg.backend.BackendException.Reason.SERVICE_NOT_RUNNING -> BackendError.ServiceNotRunning - } + return when (this.reason) { + org.amnezia.awg.backend.BackendException.Reason.VPN_NOT_AUTHORIZED -> + BackendError.Unauthorized + org.amnezia.awg.backend.BackendException.Reason.DNS_RESOLUTION_FAILURE -> BackendError.DNS + org.amnezia.awg.backend.BackendException.Reason.UNKNOWN_KERNEL_MODULE_NAME -> + BackendError.KernelModuleName + org.amnezia.awg.backend.BackendException.Reason.AWG_QUICK_CONFIG_ERROR_CODE -> + BackendError.Config + org.amnezia.awg.backend.BackendException.Reason.TUNNEL_MISSING_CONFIG -> BackendError.Config + org.amnezia.awg.backend.BackendException.Reason.UNABLE_TO_START_VPN -> + BackendError.NotAuthorized + org.amnezia.awg.backend.BackendException.Reason.TUN_CREATION_ERROR -> + BackendError.NotAuthorized + org.amnezia.awg.backend.BackendException.Reason.GO_ACTIVATION_ERROR_CODE -> + BackendError.Unknown + org.amnezia.awg.backend.BackendException.Reason.SERVICE_NOT_RUNNING -> + BackendError.ServiceNotRunning + } } fun com.wireguard.android.backend.Tunnel.State.asTunnelState(): TunnelStatus { - return when (this) { - com.wireguard.android.backend.Tunnel.State.DOWN -> TunnelStatus.Down - com.wireguard.android.backend.Tunnel.State.UP -> TunnelStatus.Up - } + return when (this) { + com.wireguard.android.backend.Tunnel.State.DOWN -> TunnelStatus.Down + com.wireguard.android.backend.Tunnel.State.UP -> TunnelStatus.Up + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/UiExtensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/UiExtensions.kt index 80d93e90..1f712d5c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/UiExtensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/UiExtensions.kt @@ -5,11 +5,9 @@ import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.common.navigation.isCurrentRoute fun NavController.goFromRoot(route: Route) { - if (currentBackStackEntry?.isCurrentRoute(route::class) == true) return - this.navigate(route) { - popUpTo(Route.Main) { - saveState = true - } - launchSingleTop = true - } + if (currentBackStackEntry?.isCurrentRoute(route::class) == true) return + this.navigate(route) { + popUpTo(Route.Main) { saveState = true } + launchSingleTop = true + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/AppViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/AppViewModel.kt index eeb9b2dd..5f81ca0c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/AppViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/AppViewModel.kt @@ -36,15 +36,20 @@ import com.zaneschepke.wireguardautotunnel.util.StringValue import com.zaneschepke.wireguardautotunnel.util.extensions.withFirstState import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent import dagger.hilt.android.lifecycle.HiltViewModel +import java.net.URL +import java.time.Instant +import java.util.* +import javax.inject.Inject +import javax.inject.Provider import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import kotlinx.coroutines.plus import kotlinx.coroutines.sync.Mutex @@ -54,608 +59,650 @@ import org.amnezia.awg.config.BadConfigException import org.amnezia.awg.config.Config import timber.log.Timber import xyz.teamgravity.pin_lock_compose.PinManager -import java.net.URL -import java.time.Instant -import java.util.* -import javax.inject.Inject -import javax.inject.Provider @HiltViewModel class AppViewModel @Inject constructor( - val appDataRepository: AppDataRepository, - @IoDispatcher private val ioDispatcher: CoroutineDispatcher, - @MainDispatcher private val mainDispatcher: CoroutineDispatcher, - @AppShell private val rootShell: Provider, - private val tunnelManager: TunnelManager, - private val serviceManager: ServiceManager, - private val logReader: LogReader, - private val fileUtils: FileUtils, - private val shortcutManager: ShortcutManager, - private val networkMonitor: NetworkMonitor, + val appDataRepository: AppDataRepository, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + @MainDispatcher private val mainDispatcher: CoroutineDispatcher, + @AppShell private val rootShell: Provider, + private val tunnelManager: TunnelManager, + private val serviceManager: ServiceManager, + private val logReader: LogReader, + private val fileUtils: FileUtils, + private val shortcutManager: ShortcutManager, + private val networkMonitor: NetworkMonitor, ) : ViewModel() { - private val tunnelMutex = Mutex() - private val settingsMutex = Mutex() - private val tunControlMutex = Mutex() - - private val _screenCallback = MutableStateFlow<(() -> Unit)?>(null) - - private val _appViewState = MutableStateFlow(AppViewState()) - val appViewState = _appViewState.asStateFlow() - - private val _logs = MutableStateFlow>(emptyList()) - val logs: StateFlow> = _logs.asStateFlow() - private val maxLogSize = 10_000 - - val uiState = - combine( - appDataRepository.settings.flow, - appDataRepository.tunnels.flow, - appDataRepository.appState.flow, - tunnelManager.activeTunnels, - serviceManager.autoTunnelActive, - networkMonitor.networkStatusFlow, - ) { array -> - val settings = array[0] as AppSettings - val tunnels = array[1] as List - val generalState = array[2] as AppState - val activeTunnels = array[3] as Map - val autoTunnel = array[4] as Boolean - val network = array[5] as NetworkStatus - - AppUiState( - appSettings = settings, - tunnels = tunnels, - activeTunnels = activeTunnels, - appState = generalState, - isAutoTunnelActive = autoTunnel, - isAppLoaded = true, - networkStatus = network, - ) - }.stateIn( - viewModelScope + ioDispatcher, - SharingStarted.Companion.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), - AppUiState(), - ) - - init { - viewModelScope.launch(ioDispatcher) { - uiState.withFirstState { state -> - initPin(state.appState.isPinLockEnabled) - handleKillSwitchChange(state.appSettings) - initServicesFromSavedState(state) - if (state.appState.isLocalLogsEnabled) startCollectingLogs() - } - } - } - - fun handleEvent(event: AppEvent) = viewModelScope.launch(ioDispatcher) { - uiState.withFirstState { state -> - when (event) { - AppEvent.ToggleLocalLogging -> handleToggleLocalLogging(state.appState.isLocalLogsEnabled) - is AppEvent.SetDebounceDelay -> handleSetDebounceDelay(state.appSettings, event.delay) - is AppEvent.CopyTunnel -> handleCopyTunnel(event.tunnel, state.tunnels) - is AppEvent.DeleteTunnel -> handleDeleteTunnel(event.tunnel, state) - is AppEvent.ImportTunnelFromClipboard -> handleClipboardImport(event.text, state.tunnels) - is AppEvent.ImportTunnelFromFile -> handleImportTunnelFromFile(event.data, state.tunnels) - is AppEvent.ImportTunnelFromUrl -> handleImportTunnelFromUrl(event.url, state.tunnels) - is AppEvent.ImportTunnelFromQrCode -> handleImportTunnelFromQr(event.qrCode, state.tunnels) - AppEvent.SetBatteryOptimizeDisableShown -> setBatteryOptimizeDisableShown() - is AppEvent.StartTunnel -> handleStartTunnel(event.tunnel, state.appSettings) - is AppEvent.StopTunnel -> handleStopTunnel(event.tunnel) - AppEvent.ToggleAutoTunnel -> handleToggleAutoTunnel(state) - AppEvent.ToggleTunnelStatsExpanded -> handleToggleExpandTunnelStats(state.appState.isTunnelStatsExpanded) - AppEvent.ToggleAlwaysOn -> handleToggleAlwaysOnVPN(state.appSettings) - AppEvent.TogglePinLock -> handlePinLockToggled(state.appState.isPinLockEnabled) - AppEvent.SetLocationDisclosureShown -> setLocationDisclosureShown() - is AppEvent.SetLocale -> handleLocaleChange(event.localeTag) - AppEvent.ToggleRestartAtBoot -> handleToggleRestartAtBoot(state.appSettings) - AppEvent.ToggleVpnKillSwitch -> handleToggleVpnKillSwitch(state.appSettings) - AppEvent.ToggleLanOnKillSwitch -> handleToggleLanOnKillSwitch(state.appSettings) - AppEvent.ToggleAppShortcuts -> handleToggleAppShortcuts(state.appSettings) - AppEvent.ToggleKernelMode -> handleToggleKernelMode(state.appSettings) - is AppEvent.SetTheme -> handleThemeChange(event.theme) - is AppEvent.ToggleIpv4Preferred -> handleToggleIpv4(event.tunnel) - is AppEvent.TogglePrimaryTunnel -> handleTogglePrimaryTunnel(event.tunnel) - is AppEvent.SetTunnelPingCooldown -> handlePingCoolDownChange(event.tunnel, event.pingCooldown) - is AppEvent.SetTunnelPingInterval -> handlePingIntervalChange(event.tunnel, event.pingInterval) - is AppEvent.AddTunnelRunSSID -> handleAddTunnelRunSSID(event.ssid, event.tunnel, state.tunnels) - is AppEvent.DeleteTunnelRunSSID -> handleRemoveTunnelRunSSID(event.ssid, event.tunnel) - is AppEvent.ToggleEthernetTunnel -> handleToggleEthernetTunnel(event.tunnel) - is AppEvent.ToggleMobileDataTunnel -> handleToggleMobileDataTunnel(event.tunnel) - AppEvent.ToggleAutoTunnelOnCellular -> handleToggleAutoTunnelOnCellular(state.appSettings) - AppEvent.ToggleAutoTunnelOnWifi -> handleToggleAutoTunnelOnWifi(state.appSettings) - is AppEvent.DeleteTrustedSSID -> handleDeleteTrustedSSID(event.ssid, state.appSettings) - AppEvent.ToggleAutoTunnelWildcards -> handleToggleAutoTunnelWildcards(state.appSettings) - AppEvent.ToggleRootShellWifi -> handleToggleRootShellWifi(state.appSettings) - is AppEvent.SaveTrustedSSID -> handleSaveTrustedSSID(event.ssid, state.appSettings) - AppEvent.ToggleAutoTunnelOnEthernet -> handleToggleTunnelOnEthernet(state.appSettings) - AppEvent.ToggleStopKillSwitchOnTrusted -> handleToggleStopKillSwitchOnTrusted(state.appSettings) - AppEvent.ToggleStopTunnelOnNoInternet -> handleToggleStopOnNoInternet(state.appSettings) - is AppEvent.ExportTunnels -> handleExportTunnels(event.configType, state.tunnels) - AppEvent.ExportLogs -> handleExportLogs() - AppEvent.MessageShown -> handleErrorShown() - is AppEvent.TogglePingTunnelEnabled -> handleTogglePingTunnel(event.tunnel) - is AppEvent.SetTunnelPingIp -> handleTunnelPingIpChange(event.tunnelConf, event.ip) - AppEvent.ToggleBottomSheet -> handleToggleBottomSheet() - AppEvent.DeleteLogs -> handleDeleteLogs() - is AppEvent.SetScreenAction -> _screenCallback.update { event.callback } - AppEvent.InvokeScreenAction -> _screenCallback.value?.invoke() - is AppEvent.SetSelectedTunnel -> _appViewState.update { it.copy(selectedTunnel = event.tunnel) } - AppEvent.VpnPermissionRequested -> requestVpnPermission(false) - is AppEvent.AppReadyCheck -> handleAppReadyCheck(event.tunnels) - is AppEvent.ShowMessage -> handleShowMessage(event.message) - is AppEvent.PopBackStack -> _appViewState.update { it.copy(popBackStack = event.pop) } - is AppEvent.ClearTunnelError -> tunnelManager.clearError(event.tunnel) - AppEvent.ToggleRemoteControl -> handleToggleRemoteControl(state.appState) - } - } - } - - private suspend fun handleToggleRemoteControl(appState: AppState) { - val enabled = !appState.isRemoteControlEnabled - if (enabled) appDataRepository.appState.setRemoteKey(UUID.randomUUID().toString()) - appDataRepository.appState.setIsRemoteControlEnabled(enabled) - } - - private fun startCollectingLogs() { - viewModelScope.launch { - logReader.bufferedLogs - .flowOn(ioDispatcher) - .collect { logMessage -> - _logs.update { currentList -> - val newList = currentList.toMutableList() - if (newList.size >= maxLogSize) { - newList.removeAt(0) - } - newList.add(logMessage) - newList - } - } - } - } - - private suspend fun handleAppReadyCheck(tunnels: List) { - if (tunnels.size == appDataRepository.tunnels.count()) { - _appViewState.update { it.copy(isAppReady = true) } - } - } - - private fun handleToggleBottomSheet() = _appViewState.update { - it.copy(showBottomSheet = !it.showBottomSheet) - } - - private suspend fun handleTunnelPingIpChange(tunnelConf: TunnelConf, ip: String) = saveTunnel( - tunnelConf.copy(pingIp = ip), - ) - - private suspend fun handleTogglePingTunnel(tunnel: TunnelConf) = saveTunnel( - tunnel.copy(isPingEnabled = !tunnel.isPingEnabled), - ) - - private suspend fun handleToggleLocalLogging(currentlyEnabled: Boolean) { - val enable = !currentlyEnabled - appDataRepository.appState.setLocalLogsEnabled(enable) - withContext(mainDispatcher) { - if (enable) { - logReader.start() - startCollectingLogs() - } else { - logReader.stop() - _logs.update { emptyList() } - } - } - } - - private suspend fun handleSetDebounceDelay(appSettings: AppSettings, delay: Int) = saveSettings( - appSettings.copy(debounceDelaySeconds = delay), - ) - - private suspend fun handleCopyTunnel(tunnel: TunnelConf, existingTunnels: List) = saveTunnel( - TunnelConf( - tunName = tunnel.generateUniqueName(existingTunnels.map { it.tunName }), - wgQuick = tunnel.wgQuick, - amQuick = tunnel.amQuick, - ), - ) - - private suspend fun handleDeleteTunnel(tunnel: TunnelConf, state: AppUiState) { - if (state.tunnels.size == 1 || tunnel.isPrimaryTunnel) { - serviceManager.stopAutoTunnel() - } - appDataRepository.tunnels.delete(tunnel) - } - - private fun requestVpnPermission(request: Boolean) = _appViewState.update { - it.copy(requestVpnPermission = request) - } - - private fun requestBatteryPermission(request: Boolean) = _appViewState.update { - it.copy(requestBatteryPermission = request) - } - - private suspend fun handleStartTunnel(tunnel: TunnelConf, appSettings: AppSettings) { - tunControlMutex.withLock { - if (!tunnelManager.hasVpnPermission() && !appSettings.isKernelEnabled) return@withLock requestVpnPermission(true) - tunnelManager.startTunnel(tunnel) - } - } - - private suspend fun handleStopTunnel(tunnel: TunnelConf) { - tunControlMutex.withLock { - tunnelManager.stopTunnel(tunnel) - } - } - - private suspend fun handleToggleAutoTunnel(state: AppUiState) { - tunControlMutex.withLock { - if (!state.appSettings.isAutoTunnelEnabled && !tunnelManager.hasVpnPermission() && - !state.appSettings.isKernelEnabled - ) { - return@withLock requestVpnPermission( - true, - ) - } - if (!state.appState.isBatteryOptimizationDisableShown) return@withLock requestBatteryPermission(true) - serviceManager.toggleAutoTunnel() - } - } - - private suspend fun handleToggleExpandTunnelStats(currentlyEnabled: Boolean) { - appDataRepository.appState.setTunnelStatsExpanded(!currentlyEnabled) - } - - private fun handleErrorShown() { - _appViewState.update { it.copy(errorMessage = null) } - } - - private fun handleShowMessage(message: StringValue) { - _appViewState.update { it.copy(errorMessage = message) } - } - - private fun popBackStack() { - _appViewState.update { it.copy(popBackStack = true) } - } - - private suspend fun handleImportTunnelFromFile(uri: Uri, tunnels: List) { - runCatching { - val tunnelConfigs = fileUtils.buildTunnelsFromUri(uri) - val existingNames = tunnels.map { it.tunName }.toMutableList() - val uniqueTunnelConfigs = tunnelConfigs.map { config -> - val uniqueName = config.generateUniqueName(existingNames) - existingNames.add(uniqueName) - config.copy(tunName = uniqueName) - } - appDataRepository.tunnels.saveAll(uniqueTunnelConfigs) - }.onFailure { - when (it) { - is FileReadException, is BadConfigException -> handleShowMessage(StringValue.StringResource(R.string.error_file_format)) - is InvalidFileExtensionException -> handleShowMessage(StringValue.StringResource(R.string.error_file_extension)) - else -> handleShowMessage(StringValue.StringResource(R.string.unknown_error)) - } - Timber.e(it) - } - } - - private suspend fun handleClipboardImport(config: String, tunnels: List) { - runCatching { - val amConfig = TunnelConf.configFromAmQuick(config) - val tunnelConf = TunnelConf.tunnelConfigFromAmConfig(amConfig) - saveTunnel(tunnelConf.copy(tunName = tunnelConf.generateUniqueName(tunnels.map { it.tunName }))) - }.onFailure { - Timber.e(it) - handleShowMessage(StringValue.StringResource(R.string.error_file_format)) - } - } - - private suspend fun handleImportTunnelFromUrl(urlString: String, tunnels: List) { - runCatching { - val url = URL(urlString) - val fileName = urlString.substringAfterLast("/") - if (!fileName.endsWith(Constants.CONF_FILE_EXTENSION)) { - throw InvalidFileExtensionException - } - url.openStream().use { stream -> - val amConfig = Config.parse(stream) - val tunnelConf = TunnelConf.tunnelConfigFromAmConfig(amConfig) - saveTunnel(tunnelConf.copy(tunName = tunnelConf.generateUniqueName(tunnels.map { it.tunName }))) - } - }.onFailure { - Timber.e(it) - val message = when (it) { - is InvalidFileExtensionException -> StringValue.StringResource(R.string.error_file_extension) - else -> StringValue.StringResource(R.string.error_download_failed) - } - handleShowMessage(message) - } - } - - private suspend fun handleImportTunnelFromQr(result: String, existingTunnels: List) { - handleClipboardImport(result, existingTunnels) - popBackStack() - } - - private suspend fun setBatteryOptimizeDisableShown() { - requestBatteryPermission(false) - appDataRepository.appState.setBatteryOptimizationDisableShown(true) - } - - private fun initServicesFromSavedState(state: AppUiState) = viewModelScope.launch(ioDispatcher) { - tunControlMutex.withLock { - if (state.appSettings.isAutoTunnelEnabled) serviceManager.startAutoTunnel() - state.tunnels.filter { it.isActive }.forEach { - tunnelManager.startTunnel(it) - } - } - } - - private fun initPin(enabled: Boolean) { - if (enabled) PinManager.initialize(WireGuardAutoTunnel.instance) - } - - private suspend fun handlePinLockToggled(currentlyEnabled: Boolean) { - if (currentlyEnabled) PinManager.clearPin() - appDataRepository.appState.setPinLockEnabled(!currentlyEnabled) - } - - private suspend fun setLocationDisclosureShown() { - appDataRepository.appState.setLocationDisclosureShown(true) - } - - private suspend fun handleToggleAlwaysOnVPN(appSettings: AppSettings) = saveSettings( - appSettings.copy( - isAlwaysOnVpnEnabled = !appSettings.isAlwaysOnVpnEnabled, - ), - ) - - private suspend fun handleLocaleChange(localeTag: String) { - appDataRepository.appState.setLocale(localeTag) - LocaleUtil.changeLocale(localeTag) - _appViewState.update { it.copy(isConfigChanged = true) } - } - - private suspend fun handleToggleRestartAtBoot(appSettings: AppSettings) = saveSettings( - appSettings.copy( - isRestoreOnBootEnabled = !appSettings.isRestoreOnBootEnabled, - ), - ) - - private suspend fun handleToggleVpnKillSwitch(appSettings: AppSettings) { - val enabled = !appSettings.isVpnKillSwitchEnabled - if (enabled && !tunnelManager.hasVpnPermission()) return requestVpnPermission(true) - val updatedSettings = appSettings.copy( - isVpnKillSwitchEnabled = enabled, - isLanOnKillSwitchEnabled = if (enabled) appSettings.isLanOnKillSwitchEnabled else false, - ) - saveSettings(updatedSettings) - handleKillSwitchChange(updatedSettings) - } - - private suspend fun handleToggleLanOnKillSwitch(appSettings: AppSettings) { - val updatedSettings = appSettings.copy( - isLanOnKillSwitchEnabled = !appSettings.isLanOnKillSwitchEnabled, - ) - saveSettings(updatedSettings) - handleKillSwitchChange(appSettings) - } - - private fun handleKillSwitchChange(appSettings: AppSettings) { - if (!appSettings.isVpnKillSwitchEnabled) return tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList()) - Timber.d("Starting kill switch") - val allowedIps = if (appSettings.isLanOnKillSwitchEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS else emptyList() - tunnelManager.setBackendState(BackendState.KILL_SWITCH_ACTIVE, allowedIps) - } - - private suspend fun handleToggleAppShortcuts(appSettings: AppSettings) { - val enabled = !appSettings.isShortcutsEnabled - if (enabled) shortcutManager.addShortcuts() else shortcutManager.removeShortcuts() - saveSettings( - appSettings.copy( - isShortcutsEnabled = enabled, - ), - ) - } - - private suspend fun handleTogglePrimaryTunnel(tunnelConf: TunnelConf) { - tunnelMutex.withLock { - appDataRepository.tunnels.updatePrimaryTunnel( - when (tunnelConf.isPrimaryTunnel) { - true -> null - false -> tunnelConf - }, - ) - } - } - - private suspend fun handleToggleIpv4(tunnelConf: TunnelConf) = saveTunnel( - tunnelConf.copy( - isIpv4Preferred = !tunnelConf.isIpv4Preferred, - ), - ) - - private suspend fun handlePingIntervalChange(tunnelConf: TunnelConf, interval: String) = saveTunnel( - tunnelConf.copy(pingInterval = if (interval.isBlank()) null else interval.toLong() * 1000), - ) - - private suspend fun handlePingCoolDownChange(tunnelConf: TunnelConf, cooldown: String) = saveTunnel( - tunnelConf.copy(pingCooldown = if (cooldown.isBlank()) null else cooldown.toLong() * 1000), - ) - - private suspend fun handleThemeChange(theme: Theme) { - appDataRepository.appState.setTheme(theme) - } - - private suspend fun handleToggleKernelMode(appSettings: AppSettings) { - val enabled = !appSettings.isKernelEnabled - if (enabled && !isKernelSupported()) { - handleShowMessage(StringValue.StringResource(R.string.kernel_not_supported)) - return - } - if (enabled && !requestRoot()) return - // disable kill switch feature in kernel mode - tunnelManager.setBackendState(BackendState.INACTIVE, emptyList()) - saveSettings( - appSettings.copy( - isKernelEnabled = enabled, - isVpnKillSwitchEnabled = false, - isLanOnKillSwitchEnabled = false, - ), - ) - } - - private suspend fun handleRemoveTunnelRunSSID(ssid: String, tunnelConfig: TunnelConf) = saveTunnel( - tunnelConfig.copy( - tunnelNetworks = (tunnelConfig.tunnelNetworks - ssid).toMutableList(), - ), - ) - - private suspend fun handleAddTunnelRunSSID(ssid: String, tunnelConf: TunnelConf, existingTunnels: List) { - if (ssid.isBlank()) return - val trimmed = ssid.trim() - if (existingTunnels.any { it.tunnelNetworks.contains(trimmed) }) return handleShowMessage(StringValue.StringResource(R.string.error_ssid_exists)) - saveTunnel( - tunnelConf.copy( - tunnelNetworks = (tunnelConf.tunnelNetworks + ssid).toMutableList(), - ), - ) - } - - private suspend fun handleToggleMobileDataTunnel(tunnelConf: TunnelConf) { - tunnelMutex.withLock { - if (tunnelConf.isMobileDataTunnel) return appDataRepository.tunnels.updateMobileDataTunnel(null) - appDataRepository.tunnels.updateMobileDataTunnel(tunnelConf) - } - } - - private suspend fun handleToggleEthernetTunnel(tunnelConf: TunnelConf) { - tunnelMutex.withLock { - if (tunnelConf.isEthernetTunnel) return appDataRepository.tunnels.updateEthernetTunnel(null) - appDataRepository.tunnels.updateEthernetTunnel(tunnelConf) - } - } - - private suspend fun handleToggleAutoTunnelOnWifi(appSettings: AppSettings) = saveSettings( - appSettings.copy( - isTunnelOnWifiEnabled = !appSettings.isTunnelOnWifiEnabled, - ), - ) - - private suspend fun handleToggleAutoTunnelOnCellular(appSettings: AppSettings) = saveSettings( - appSettings.copy( - isTunnelOnMobileDataEnabled = !appSettings.isTunnelOnMobileDataEnabled, - ), - ) - - private suspend fun handleToggleAutoTunnelWildcards(appSettings: AppSettings) = saveSettings( - appSettings.copy( - isWildcardsEnabled = !appSettings.isWildcardsEnabled, - ), - ) - - private suspend fun handleDeleteTrustedSSID(ssid: String, appSettings: AppSettings) = saveSettings( - appSettings.copy( - trustedNetworkSSIDs = (appSettings.trustedNetworkSSIDs - ssid).toMutableList(), - ), - ) - - private suspend fun handleToggleRootShellWifi(appSettings: AppSettings) { - if (requestRoot()) { - saveSettings( - appSettings.copy(isWifiNameByShellEnabled = !appSettings.isWifiNameByShellEnabled), - ) - } - } - - private suspend fun handleToggleTunnelOnEthernet(appSettings: AppSettings) = saveSettings( - appSettings.copy(isTunnelOnEthernetEnabled = !appSettings.isTunnelOnEthernetEnabled), - ) - - private suspend fun handleSaveTrustedSSID(ssid: String, appSettings: AppSettings) { - if (ssid.isEmpty()) return - val trimmed = ssid.trim() - if (appSettings.trustedNetworkSSIDs.contains(trimmed)) return handleShowMessage(StringValue.StringResource(R.string.error_ssid_exists)) - saveSettings( - appSettings.copy( - trustedNetworkSSIDs = (appSettings.trustedNetworkSSIDs + ssid).toMutableList(), - ), - ) - } - - private suspend fun handleToggleStopOnNoInternet(appSettings: AppSettings) = saveSettings( - appSettings.copy(isStopOnNoInternetEnabled = !appSettings.isStopOnNoInternetEnabled), - ) - - private suspend fun handleToggleStopKillSwitchOnTrusted(appSettings: AppSettings) = saveSettings( - appSettings.copy(isDisableKillSwitchOnTrustedEnabled = !appSettings.isDisableKillSwitchOnTrustedEnabled), - ) - - private suspend fun isKernelSupported(): Boolean { - return withContext(ioDispatcher) { - WgQuickBackend.hasKernelSupport() - } - } - - private suspend fun saveSettings(appSettings: AppSettings) = withContext(ioDispatcher) { - settingsMutex.withLock { - appDataRepository.settings.save(appSettings) - } - } - - private suspend fun saveTunnel(tunnel: TunnelConf) = withContext(ioDispatcher) { - tunnelMutex.withLock { - appDataRepository.tunnels.save(tunnel) - } - } - - private suspend fun handleExportTunnels(configType: ConfigType, tunnels: List) { - runCatching { - val (files, shareFileName) = when (configType) { - ConfigType.AMNEZIA -> { - Pair(fileUtils.createAmFiles(tunnels), "am-export_${Instant.now().epochSecond}.zip") - } - ConfigType.WG -> { - Pair(fileUtils.createWgFiles(tunnels), "wg-export_${Instant.now().epochSecond}.zip") - } - } - val shareFile = fileUtils.createNewShareFile(shareFileName) - fileUtils.zipAll(shareFile, files) - fileUtils.shareFile(shareFile) - }.onFailure { - Timber.e(it) - handleShowMessage(StringValue.StringResource(R.string.export_failed)) - } - } - - private suspend fun handleExportLogs() { - runCatching { - val file = fileUtils.createNewShareFile("${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.zip") - logReader.zipLogFiles(file.absolutePath) - fileUtils.shareFile(file) - }.onFailure { - Timber.e(it) - handleShowMessage(StringValue.StringResource(R.string.export_failed)) - } - } - - private suspend fun handleDeleteLogs() { - withContext(mainDispatcher) { - logReader.deleteAndClearLogs() - _logs.update { emptyList() } - } - } - - private suspend fun requestRoot(): Boolean { - return withContext(ioDispatcher) { - try { - rootShell.get().start() - handleShowMessage(StringValue.StringResource(R.string.root_accepted)) - true - } catch (e: Exception) { - handleShowMessage(StringValue.StringResource(R.string.error_root_denied)) - false - } - } - } + private val tunnelMutex = Mutex() + private val settingsMutex = Mutex() + private val tunControlMutex = Mutex() + + private val _screenCallback = MutableStateFlow<(() -> Unit)?>(null) + + private val _appViewState = MutableStateFlow(AppViewState()) + val appViewState = _appViewState.asStateFlow() + + private val _logs = MutableStateFlow>(emptyList()) + val logs: StateFlow> = _logs.asStateFlow() + private val maxLogSize = 10_000 + + val uiState = + combine( + appDataRepository.settings.flow, + appDataRepository.tunnels.flow, + appDataRepository.appState.flow, + tunnelManager.activeTunnels, + serviceManager.autoTunnelActive, + networkMonitor.networkStatusFlow, + ) { array -> + val settings = array[0] as AppSettings + val tunnels = array[1] as List + val generalState = array[2] as AppState + val activeTunnels = array[3] as Map + val autoTunnel = array[4] as Boolean + val network = array[5] as NetworkStatus + + AppUiState( + appSettings = settings, + tunnels = tunnels, + activeTunnels = activeTunnels, + appState = generalState, + isAutoTunnelActive = autoTunnel, + isAppLoaded = true, + networkStatus = network, + ) + } + .stateIn( + viewModelScope + ioDispatcher, + SharingStarted.Companion.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), + AppUiState(), + ) + + init { + viewModelScope.launch(ioDispatcher) { + uiState.withFirstState { state -> + initPin(state.appState.isPinLockEnabled) + handleKillSwitchChange(state.appSettings) + initServicesFromSavedState(state) + if (state.appState.isLocalLogsEnabled) startCollectingLogs() + } + } + } + + fun handleEvent(event: AppEvent) = + viewModelScope.launch(ioDispatcher) { + uiState.withFirstState { state -> + when (event) { + AppEvent.ToggleLocalLogging -> + handleToggleLocalLogging(state.appState.isLocalLogsEnabled) + is AppEvent.SetDebounceDelay -> + handleSetDebounceDelay(state.appSettings, event.delay) + is AppEvent.CopyTunnel -> handleCopyTunnel(event.tunnel, state.tunnels) + is AppEvent.DeleteTunnel -> handleDeleteTunnel(event.tunnel, state) + is AppEvent.ImportTunnelFromClipboard -> + handleClipboardImport(event.text, state.tunnels) + is AppEvent.ImportTunnelFromFile -> + handleImportTunnelFromFile(event.data, state.tunnels) + is AppEvent.ImportTunnelFromUrl -> + handleImportTunnelFromUrl(event.url, state.tunnels) + is AppEvent.ImportTunnelFromQrCode -> + handleImportTunnelFromQr(event.qrCode, state.tunnels) + AppEvent.SetBatteryOptimizeDisableShown -> setBatteryOptimizeDisableShown() + is AppEvent.StartTunnel -> handleStartTunnel(event.tunnel, state.appSettings) + is AppEvent.StopTunnel -> handleStopTunnel(event.tunnel) + AppEvent.ToggleAutoTunnel -> handleToggleAutoTunnel(state) + AppEvent.ToggleTunnelStatsExpanded -> + handleToggleExpandTunnelStats(state.appState.isTunnelStatsExpanded) + AppEvent.ToggleAlwaysOn -> handleToggleAlwaysOnVPN(state.appSettings) + AppEvent.TogglePinLock -> handlePinLockToggled(state.appState.isPinLockEnabled) + AppEvent.SetLocationDisclosureShown -> setLocationDisclosureShown() + is AppEvent.SetLocale -> handleLocaleChange(event.localeTag) + AppEvent.ToggleRestartAtBoot -> handleToggleRestartAtBoot(state.appSettings) + AppEvent.ToggleVpnKillSwitch -> handleToggleVpnKillSwitch(state.appSettings) + AppEvent.ToggleLanOnKillSwitch -> handleToggleLanOnKillSwitch(state.appSettings) + AppEvent.ToggleAppShortcuts -> handleToggleAppShortcuts(state.appSettings) + AppEvent.ToggleKernelMode -> handleToggleKernelMode(state.appSettings) + is AppEvent.SetTheme -> handleThemeChange(event.theme) + is AppEvent.ToggleIpv4Preferred -> handleToggleIpv4(event.tunnel) + is AppEvent.TogglePrimaryTunnel -> handleTogglePrimaryTunnel(event.tunnel) + is AppEvent.SetTunnelPingCooldown -> + handlePingCoolDownChange(event.tunnel, event.pingCooldown) + is AppEvent.SetTunnelPingInterval -> + handlePingIntervalChange(event.tunnel, event.pingInterval) + is AppEvent.AddTunnelRunSSID -> + handleAddTunnelRunSSID(event.ssid, event.tunnel, state.tunnels) + is AppEvent.DeleteTunnelRunSSID -> + handleRemoveTunnelRunSSID(event.ssid, event.tunnel) + is AppEvent.ToggleEthernetTunnel -> handleToggleEthernetTunnel(event.tunnel) + is AppEvent.ToggleMobileDataTunnel -> handleToggleMobileDataTunnel(event.tunnel) + AppEvent.ToggleAutoTunnelOnCellular -> + handleToggleAutoTunnelOnCellular(state.appSettings) + AppEvent.ToggleAutoTunnelOnWifi -> + handleToggleAutoTunnelOnWifi(state.appSettings) + is AppEvent.DeleteTrustedSSID -> + handleDeleteTrustedSSID(event.ssid, state.appSettings) + AppEvent.ToggleAutoTunnelWildcards -> + handleToggleAutoTunnelWildcards(state.appSettings) + AppEvent.ToggleRootShellWifi -> handleToggleRootShellWifi(state.appSettings) + is AppEvent.SaveTrustedSSID -> + handleSaveTrustedSSID(event.ssid, state.appSettings) + AppEvent.ToggleAutoTunnelOnEthernet -> + handleToggleTunnelOnEthernet(state.appSettings) + AppEvent.ToggleStopKillSwitchOnTrusted -> + handleToggleStopKillSwitchOnTrusted(state.appSettings) + AppEvent.ToggleStopTunnelOnNoInternet -> + handleToggleStopOnNoInternet(state.appSettings) + is AppEvent.ExportTunnels -> + handleExportTunnels(event.configType, state.tunnels) + AppEvent.ExportLogs -> handleExportLogs() + AppEvent.MessageShown -> handleErrorShown() + is AppEvent.TogglePingTunnelEnabled -> handleTogglePingTunnel(event.tunnel) + is AppEvent.SetTunnelPingIp -> + handleTunnelPingIpChange(event.tunnelConf, event.ip) + AppEvent.ToggleBottomSheet -> handleToggleBottomSheet() + AppEvent.DeleteLogs -> handleDeleteLogs() + is AppEvent.SetScreenAction -> _screenCallback.update { event.callback } + AppEvent.InvokeScreenAction -> _screenCallback.value?.invoke() + is AppEvent.SetSelectedTunnel -> + _appViewState.update { it.copy(selectedTunnel = event.tunnel) } + AppEvent.VpnPermissionRequested -> requestVpnPermission(false) + is AppEvent.AppReadyCheck -> handleAppReadyCheck(event.tunnels) + is AppEvent.ShowMessage -> handleShowMessage(event.message) + is AppEvent.PopBackStack -> + _appViewState.update { it.copy(popBackStack = event.pop) } + is AppEvent.ClearTunnelError -> tunnelManager.clearError(event.tunnel) + AppEvent.ToggleRemoteControl -> handleToggleRemoteControl(state.appState) + } + } + } + + private suspend fun handleToggleRemoteControl(appState: AppState) { + val enabled = !appState.isRemoteControlEnabled + if (enabled) appDataRepository.appState.setRemoteKey(UUID.randomUUID().toString()) + appDataRepository.appState.setIsRemoteControlEnabled(enabled) + } + + private fun startCollectingLogs() { + viewModelScope.launch { + logReader.bufferedLogs.flowOn(ioDispatcher).collect { logMessage -> + _logs.update { currentList -> + val newList = currentList.toMutableList() + if (newList.size >= maxLogSize) { + newList.removeAt(0) + } + newList.add(logMessage) + newList + } + } + } + } + + private suspend fun handleAppReadyCheck(tunnels: List) { + if (tunnels.size == appDataRepository.tunnels.count()) { + _appViewState.update { it.copy(isAppReady = true) } + } + } + + private fun handleToggleBottomSheet() = + _appViewState.update { it.copy(showBottomSheet = !it.showBottomSheet) } + + private suspend fun handleTunnelPingIpChange(tunnelConf: TunnelConf, ip: String) = + saveTunnel(tunnelConf.copy(pingIp = ip)) + + private suspend fun handleTogglePingTunnel(tunnel: TunnelConf) = + saveTunnel(tunnel.copy(isPingEnabled = !tunnel.isPingEnabled)) + + private suspend fun handleToggleLocalLogging(currentlyEnabled: Boolean) { + val enable = !currentlyEnabled + appDataRepository.appState.setLocalLogsEnabled(enable) + withContext(mainDispatcher) { + if (enable) { + logReader.start() + startCollectingLogs() + } else { + logReader.stop() + _logs.update { emptyList() } + } + } + } + + private suspend fun handleSetDebounceDelay(appSettings: AppSettings, delay: Int) = + saveSettings(appSettings.copy(debounceDelaySeconds = delay)) + + private suspend fun handleCopyTunnel(tunnel: TunnelConf, existingTunnels: List) = + saveTunnel( + TunnelConf( + tunName = tunnel.generateUniqueName(existingTunnels.map { it.tunName }), + wgQuick = tunnel.wgQuick, + amQuick = tunnel.amQuick, + ) + ) + + private suspend fun handleDeleteTunnel(tunnel: TunnelConf, state: AppUiState) { + if (state.tunnels.size == 1 || tunnel.isPrimaryTunnel) { + serviceManager.stopAutoTunnel() + } + appDataRepository.tunnels.delete(tunnel) + } + + private fun requestVpnPermission(request: Boolean) = + _appViewState.update { it.copy(requestVpnPermission = request) } + + private fun requestBatteryPermission(request: Boolean) = + _appViewState.update { it.copy(requestBatteryPermission = request) } + + private suspend fun handleStartTunnel(tunnel: TunnelConf, appSettings: AppSettings) { + tunControlMutex.withLock { + if (!tunnelManager.hasVpnPermission() && !appSettings.isKernelEnabled) + return@withLock requestVpnPermission(true) + tunnelManager.startTunnel(tunnel) + } + } + + private suspend fun handleStopTunnel(tunnel: TunnelConf) { + tunControlMutex.withLock { tunnelManager.stopTunnel(tunnel) } + } + + private suspend fun handleToggleAutoTunnel(state: AppUiState) { + tunControlMutex.withLock { + if ( + !state.appSettings.isAutoTunnelEnabled && + !tunnelManager.hasVpnPermission() && + !state.appSettings.isKernelEnabled + ) { + return@withLock requestVpnPermission(true) + } + if (!state.appState.isBatteryOptimizationDisableShown) + return@withLock requestBatteryPermission(true) + serviceManager.toggleAutoTunnel() + } + } + + private suspend fun handleToggleExpandTunnelStats(currentlyEnabled: Boolean) { + appDataRepository.appState.setTunnelStatsExpanded(!currentlyEnabled) + } + + private fun handleErrorShown() { + _appViewState.update { it.copy(errorMessage = null) } + } + + private fun handleShowMessage(message: StringValue) { + _appViewState.update { it.copy(errorMessage = message) } + } + + private fun popBackStack() { + _appViewState.update { it.copy(popBackStack = true) } + } + + private suspend fun handleImportTunnelFromFile(uri: Uri, tunnels: List) { + runCatching { + val tunnelConfigs = fileUtils.buildTunnelsFromUri(uri) + val existingNames = tunnels.map { it.tunName }.toMutableList() + val uniqueTunnelConfigs = + tunnelConfigs.map { config -> + val uniqueName = config.generateUniqueName(existingNames) + existingNames.add(uniqueName) + config.copy(tunName = uniqueName) + } + appDataRepository.tunnels.saveAll(uniqueTunnelConfigs) + } + .onFailure { + when (it) { + is FileReadException, + is BadConfigException -> + handleShowMessage(StringValue.StringResource(R.string.error_file_format)) + is InvalidFileExtensionException -> + handleShowMessage(StringValue.StringResource(R.string.error_file_extension)) + else -> handleShowMessage(StringValue.StringResource(R.string.unknown_error)) + } + Timber.e(it) + } + } + + private suspend fun handleClipboardImport(config: String, tunnels: List) { + runCatching { + val amConfig = TunnelConf.configFromAmQuick(config) + val tunnelConf = TunnelConf.tunnelConfigFromAmConfig(amConfig) + saveTunnel( + tunnelConf.copy( + tunName = tunnelConf.generateUniqueName(tunnels.map { it.tunName }) + ) + ) + } + .onFailure { + Timber.e(it) + handleShowMessage(StringValue.StringResource(R.string.error_file_format)) + } + } + + private suspend fun handleImportTunnelFromUrl(urlString: String, tunnels: List) { + runCatching { + val url = URL(urlString) + val fileName = urlString.substringAfterLast("/") + if (!fileName.endsWith(Constants.CONF_FILE_EXTENSION)) { + throw InvalidFileExtensionException + } + url.openStream().use { stream -> + val amConfig = Config.parse(stream) + val tunnelConf = TunnelConf.tunnelConfigFromAmConfig(amConfig) + saveTunnel( + tunnelConf.copy( + tunName = tunnelConf.generateUniqueName(tunnels.map { it.tunName }) + ) + ) + } + } + .onFailure { + Timber.e(it) + val message = + when (it) { + is InvalidFileExtensionException -> + StringValue.StringResource(R.string.error_file_extension) + else -> StringValue.StringResource(R.string.error_download_failed) + } + handleShowMessage(message) + } + } + + private suspend fun handleImportTunnelFromQr( + result: String, + existingTunnels: List, + ) { + handleClipboardImport(result, existingTunnels) + popBackStack() + } + + private suspend fun setBatteryOptimizeDisableShown() { + requestBatteryPermission(false) + appDataRepository.appState.setBatteryOptimizationDisableShown(true) + } + + private fun initServicesFromSavedState(state: AppUiState) = + viewModelScope.launch(ioDispatcher) { + tunControlMutex.withLock { + if (state.appSettings.isAutoTunnelEnabled) serviceManager.startAutoTunnel() + state.tunnels.filter { it.isActive }.forEach { tunnelManager.startTunnel(it) } + } + } + + private fun initPin(enabled: Boolean) { + if (enabled) PinManager.initialize(WireGuardAutoTunnel.instance) + } + + private suspend fun handlePinLockToggled(currentlyEnabled: Boolean) { + if (currentlyEnabled) PinManager.clearPin() + appDataRepository.appState.setPinLockEnabled(!currentlyEnabled) + } + + private suspend fun setLocationDisclosureShown() { + appDataRepository.appState.setLocationDisclosureShown(true) + } + + private suspend fun handleToggleAlwaysOnVPN(appSettings: AppSettings) = + saveSettings(appSettings.copy(isAlwaysOnVpnEnabled = !appSettings.isAlwaysOnVpnEnabled)) + + private suspend fun handleLocaleChange(localeTag: String) { + appDataRepository.appState.setLocale(localeTag) + LocaleUtil.changeLocale(localeTag) + _appViewState.update { it.copy(isConfigChanged = true) } + } + + private suspend fun handleToggleRestartAtBoot(appSettings: AppSettings) = + saveSettings(appSettings.copy(isRestoreOnBootEnabled = !appSettings.isRestoreOnBootEnabled)) + + private suspend fun handleToggleVpnKillSwitch(appSettings: AppSettings) { + val enabled = !appSettings.isVpnKillSwitchEnabled + if (enabled && !tunnelManager.hasVpnPermission()) return requestVpnPermission(true) + val updatedSettings = + appSettings.copy( + isVpnKillSwitchEnabled = enabled, + isLanOnKillSwitchEnabled = + if (enabled) appSettings.isLanOnKillSwitchEnabled else false, + ) + saveSettings(updatedSettings) + handleKillSwitchChange(updatedSettings) + } + + private suspend fun handleToggleLanOnKillSwitch(appSettings: AppSettings) { + val updatedSettings = + appSettings.copy(isLanOnKillSwitchEnabled = !appSettings.isLanOnKillSwitchEnabled) + saveSettings(updatedSettings) + handleKillSwitchChange(appSettings) + } + + private fun handleKillSwitchChange(appSettings: AppSettings) { + if (!appSettings.isVpnKillSwitchEnabled) + return tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList()) + Timber.d("Starting kill switch") + val allowedIps = + if (appSettings.isLanOnKillSwitchEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS + else emptyList() + tunnelManager.setBackendState(BackendState.KILL_SWITCH_ACTIVE, allowedIps) + } + + private suspend fun handleToggleAppShortcuts(appSettings: AppSettings) { + val enabled = !appSettings.isShortcutsEnabled + if (enabled) shortcutManager.addShortcuts() else shortcutManager.removeShortcuts() + saveSettings(appSettings.copy(isShortcutsEnabled = enabled)) + } + + private suspend fun handleTogglePrimaryTunnel(tunnelConf: TunnelConf) { + tunnelMutex.withLock { + appDataRepository.tunnels.updatePrimaryTunnel( + when (tunnelConf.isPrimaryTunnel) { + true -> null + false -> tunnelConf + } + ) + } + } + + private suspend fun handleToggleIpv4(tunnelConf: TunnelConf) = + saveTunnel(tunnelConf.copy(isIpv4Preferred = !tunnelConf.isIpv4Preferred)) + + private suspend fun handlePingIntervalChange(tunnelConf: TunnelConf, interval: String) = + saveTunnel( + tunnelConf.copy( + pingInterval = if (interval.isBlank()) null else interval.toLong() * 1000 + ) + ) + + private suspend fun handlePingCoolDownChange(tunnelConf: TunnelConf, cooldown: String) = + saveTunnel( + tunnelConf.copy( + pingCooldown = if (cooldown.isBlank()) null else cooldown.toLong() * 1000 + ) + ) + + private suspend fun handleThemeChange(theme: Theme) { + appDataRepository.appState.setTheme(theme) + } + + private suspend fun handleToggleKernelMode(appSettings: AppSettings) { + val enabled = !appSettings.isKernelEnabled + if (enabled && !isKernelSupported()) { + handleShowMessage(StringValue.StringResource(R.string.kernel_not_supported)) + return + } + if (enabled && !requestRoot()) return + // disable kill switch feature in kernel mode + tunnelManager.setBackendState(BackendState.INACTIVE, emptyList()) + saveSettings( + appSettings.copy( + isKernelEnabled = enabled, + isVpnKillSwitchEnabled = false, + isLanOnKillSwitchEnabled = false, + ) + ) + } + + private suspend fun handleRemoveTunnelRunSSID(ssid: String, tunnelConfig: TunnelConf) = + saveTunnel( + tunnelConfig.copy(tunnelNetworks = (tunnelConfig.tunnelNetworks - ssid).toMutableList()) + ) + + private suspend fun handleAddTunnelRunSSID( + ssid: String, + tunnelConf: TunnelConf, + existingTunnels: List, + ) { + if (ssid.isBlank()) return + val trimmed = ssid.trim() + if (existingTunnels.any { it.tunnelNetworks.contains(trimmed) }) + return handleShowMessage(StringValue.StringResource(R.string.error_ssid_exists)) + saveTunnel( + tunnelConf.copy(tunnelNetworks = (tunnelConf.tunnelNetworks + ssid).toMutableList()) + ) + } + + private suspend fun handleToggleMobileDataTunnel(tunnelConf: TunnelConf) { + tunnelMutex.withLock { + if (tunnelConf.isMobileDataTunnel) + return appDataRepository.tunnels.updateMobileDataTunnel(null) + appDataRepository.tunnels.updateMobileDataTunnel(tunnelConf) + } + } + + private suspend fun handleToggleEthernetTunnel(tunnelConf: TunnelConf) { + tunnelMutex.withLock { + if (tunnelConf.isEthernetTunnel) + return appDataRepository.tunnels.updateEthernetTunnel(null) + appDataRepository.tunnels.updateEthernetTunnel(tunnelConf) + } + } + + private suspend fun handleToggleAutoTunnelOnWifi(appSettings: AppSettings) = + saveSettings(appSettings.copy(isTunnelOnWifiEnabled = !appSettings.isTunnelOnWifiEnabled)) + + private suspend fun handleToggleAutoTunnelOnCellular(appSettings: AppSettings) = + saveSettings( + appSettings.copy(isTunnelOnMobileDataEnabled = !appSettings.isTunnelOnMobileDataEnabled) + ) + + private suspend fun handleToggleAutoTunnelWildcards(appSettings: AppSettings) = + saveSettings(appSettings.copy(isWildcardsEnabled = !appSettings.isWildcardsEnabled)) + + private suspend fun handleDeleteTrustedSSID(ssid: String, appSettings: AppSettings) = + saveSettings( + appSettings.copy( + trustedNetworkSSIDs = (appSettings.trustedNetworkSSIDs - ssid).toMutableList() + ) + ) + + private suspend fun handleToggleRootShellWifi(appSettings: AppSettings) { + if (requestRoot()) { + saveSettings( + appSettings.copy(isWifiNameByShellEnabled = !appSettings.isWifiNameByShellEnabled) + ) + } + } + + private suspend fun handleToggleTunnelOnEthernet(appSettings: AppSettings) = + saveSettings( + appSettings.copy(isTunnelOnEthernetEnabled = !appSettings.isTunnelOnEthernetEnabled) + ) + + private suspend fun handleSaveTrustedSSID(ssid: String, appSettings: AppSettings) { + if (ssid.isEmpty()) return + val trimmed = ssid.trim() + if (appSettings.trustedNetworkSSIDs.contains(trimmed)) + return handleShowMessage(StringValue.StringResource(R.string.error_ssid_exists)) + saveSettings( + appSettings.copy( + trustedNetworkSSIDs = (appSettings.trustedNetworkSSIDs + ssid).toMutableList() + ) + ) + } + + private suspend fun handleToggleStopOnNoInternet(appSettings: AppSettings) = + saveSettings( + appSettings.copy(isStopOnNoInternetEnabled = !appSettings.isStopOnNoInternetEnabled) + ) + + private suspend fun handleToggleStopKillSwitchOnTrusted(appSettings: AppSettings) = + saveSettings( + appSettings.copy( + isDisableKillSwitchOnTrustedEnabled = + !appSettings.isDisableKillSwitchOnTrustedEnabled + ) + ) + + private suspend fun isKernelSupported(): Boolean { + return withContext(ioDispatcher) { WgQuickBackend.hasKernelSupport() } + } + + private suspend fun saveSettings(appSettings: AppSettings) = + withContext(ioDispatcher) { + settingsMutex.withLock { appDataRepository.settings.save(appSettings) } + } + + private suspend fun saveTunnel(tunnel: TunnelConf) = + withContext(ioDispatcher) { + tunnelMutex.withLock { appDataRepository.tunnels.save(tunnel) } + } + + private suspend fun handleExportTunnels(configType: ConfigType, tunnels: List) { + runCatching { + val (files, shareFileName) = + when (configType) { + ConfigType.AMNEZIA -> { + Pair( + fileUtils.createAmFiles(tunnels), + "am-export_${Instant.now().epochSecond}.zip", + ) + } + ConfigType.WG -> { + Pair( + fileUtils.createWgFiles(tunnels), + "wg-export_${Instant.now().epochSecond}.zip", + ) + } + } + val shareFile = fileUtils.createNewShareFile(shareFileName) + fileUtils.zipAll(shareFile, files) + fileUtils.shareFile(shareFile) + } + .onFailure { + Timber.e(it) + handleShowMessage(StringValue.StringResource(R.string.export_failed)) + } + } + + private suspend fun handleExportLogs() { + runCatching { + val file = + fileUtils.createNewShareFile( + "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.zip" + ) + logReader.zipLogFiles(file.absolutePath) + fileUtils.shareFile(file) + } + .onFailure { + Timber.e(it) + handleShowMessage(StringValue.StringResource(R.string.export_failed)) + } + } + + private suspend fun handleDeleteLogs() { + withContext(mainDispatcher) { + logReader.deleteAndClearLogs() + _logs.update { emptyList() } + } + } + + private suspend fun requestRoot(): Boolean { + return withContext(ioDispatcher) { + try { + rootShell.get().start() + handleShowMessage(StringValue.StringResource(R.string.root_accepted)) + true + } catch (e: Exception) { + handleShowMessage(StringValue.StringResource(R.string.error_root_denied)) + false + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/event/AppEvent.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/event/AppEvent.kt index 9a2829a4..d33ff72c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/event/AppEvent.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/event/AppEvent.kt @@ -7,60 +7,115 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.Theme import com.zaneschepke.wireguardautotunnel.util.StringValue sealed class AppEvent { - data class SetScreenAction(val callback: () -> Unit) : AppEvent() - data object InvokeScreenAction : AppEvent() - data object ToggleLocalLogging : AppEvent() - data object ToggleRestartAtBoot : AppEvent() - data object ToggleVpnKillSwitch : AppEvent() - data object ToggleLanOnKillSwitch : AppEvent() - data object ToggleAppShortcuts : AppEvent() - data object ToggleKernelMode : AppEvent() - data object ToggleAlwaysOn : AppEvent() - data object TogglePinLock : AppEvent() - data class TogglePrimaryTunnel(val tunnel: TunnelConf) : AppEvent() - data class ToggleIpv4Preferred(val tunnel: TunnelConf) : AppEvent() - data class TogglePingTunnelEnabled(val tunnel: TunnelConf) : AppEvent() - data class SetTunnelPingInterval(val tunnel: TunnelConf, val pingInterval: String) : AppEvent() - data class SetTunnelPingCooldown(val tunnel: TunnelConf, val pingCooldown: String) : AppEvent() - data class SetTunnelPingIp(val tunnelConf: TunnelConf, val ip: String) : AppEvent() - data class AddTunnelRunSSID(val ssid: String, val tunnel: TunnelConf) : AppEvent() - data class DeleteTunnelRunSSID(val ssid: String, val tunnel: TunnelConf) : AppEvent() - data class ToggleEthernetTunnel(val tunnel: TunnelConf) : AppEvent() - data class ToggleMobileDataTunnel(val tunnel: TunnelConf) : AppEvent() - data class SetDebounceDelay(val delay: Int) : AppEvent() - data object ToggleAutoTunnel : AppEvent() - data class StartTunnel(val tunnel: TunnelConf) : AppEvent() - data class StopTunnel(val tunnel: TunnelConf) : AppEvent() - data class DeleteTunnel(val tunnel: TunnelConf) : AppEvent() - data class CopyTunnel(val tunnel: TunnelConf) : AppEvent() - data class ImportTunnelFromFile(val data: Uri) : AppEvent() - data class ImportTunnelFromClipboard(val text: String) : AppEvent() - data class ImportTunnelFromUrl(val url: String) : AppEvent() - data class ImportTunnelFromQrCode(val qrCode: String) : AppEvent() - data object ToggleTunnelStatsExpanded : AppEvent() - data object SetBatteryOptimizeDisableShown : AppEvent() - data object SetLocationDisclosureShown : AppEvent() - data class SetLocale(val localeTag: String) : AppEvent() - data class SetTheme(val theme: Theme) : AppEvent() - data object ToggleAutoTunnelOnWifi : AppEvent() - data object ToggleAutoTunnelOnCellular : AppEvent() - data object ToggleAutoTunnelOnEthernet : AppEvent() - data object ToggleStopKillSwitchOnTrusted : AppEvent() - data object ToggleStopTunnelOnNoInternet : AppEvent() - data object ToggleAutoTunnelWildcards : AppEvent() - data object ToggleRootShellWifi : AppEvent() - data class DeleteTrustedSSID(val ssid: String) : AppEvent() - data class SaveTrustedSSID(val ssid: String) : AppEvent() - data class ExportTunnels(val configType: ConfigType) : AppEvent() - data object ExportLogs : AppEvent() - data object DeleteLogs : AppEvent() - data object MessageShown : AppEvent() - data class ShowMessage(val message: StringValue) : AppEvent() - data class ClearTunnelError(val tunnel: TunnelConf) : AppEvent() - data class PopBackStack(val pop: Boolean) : AppEvent() - data object ToggleBottomSheet : AppEvent() - data class SetSelectedTunnel(val tunnel: TunnelConf?) : AppEvent() - data object VpnPermissionRequested : AppEvent() - data class AppReadyCheck(val tunnels: List) : AppEvent() - data object ToggleRemoteControl : AppEvent() + data class SetScreenAction(val callback: () -> Unit) : AppEvent() + + data object InvokeScreenAction : AppEvent() + + data object ToggleLocalLogging : AppEvent() + + data object ToggleRestartAtBoot : AppEvent() + + data object ToggleVpnKillSwitch : AppEvent() + + data object ToggleLanOnKillSwitch : AppEvent() + + data object ToggleAppShortcuts : AppEvent() + + data object ToggleKernelMode : AppEvent() + + data object ToggleAlwaysOn : AppEvent() + + data object TogglePinLock : AppEvent() + + data class TogglePrimaryTunnel(val tunnel: TunnelConf) : AppEvent() + + data class ToggleIpv4Preferred(val tunnel: TunnelConf) : AppEvent() + + data class TogglePingTunnelEnabled(val tunnel: TunnelConf) : AppEvent() + + data class SetTunnelPingInterval(val tunnel: TunnelConf, val pingInterval: String) : AppEvent() + + data class SetTunnelPingCooldown(val tunnel: TunnelConf, val pingCooldown: String) : AppEvent() + + data class SetTunnelPingIp(val tunnelConf: TunnelConf, val ip: String) : AppEvent() + + data class AddTunnelRunSSID(val ssid: String, val tunnel: TunnelConf) : AppEvent() + + data class DeleteTunnelRunSSID(val ssid: String, val tunnel: TunnelConf) : AppEvent() + + data class ToggleEthernetTunnel(val tunnel: TunnelConf) : AppEvent() + + data class ToggleMobileDataTunnel(val tunnel: TunnelConf) : AppEvent() + + data class SetDebounceDelay(val delay: Int) : AppEvent() + + data object ToggleAutoTunnel : AppEvent() + + data class StartTunnel(val tunnel: TunnelConf) : AppEvent() + + data class StopTunnel(val tunnel: TunnelConf) : AppEvent() + + data class DeleteTunnel(val tunnel: TunnelConf) : AppEvent() + + data class CopyTunnel(val tunnel: TunnelConf) : AppEvent() + + data class ImportTunnelFromFile(val data: Uri) : AppEvent() + + data class ImportTunnelFromClipboard(val text: String) : AppEvent() + + data class ImportTunnelFromUrl(val url: String) : AppEvent() + + data class ImportTunnelFromQrCode(val qrCode: String) : AppEvent() + + data object ToggleTunnelStatsExpanded : AppEvent() + + data object SetBatteryOptimizeDisableShown : AppEvent() + + data object SetLocationDisclosureShown : AppEvent() + + data class SetLocale(val localeTag: String) : AppEvent() + + data class SetTheme(val theme: Theme) : AppEvent() + + data object ToggleAutoTunnelOnWifi : AppEvent() + + data object ToggleAutoTunnelOnCellular : AppEvent() + + data object ToggleAutoTunnelOnEthernet : AppEvent() + + data object ToggleStopKillSwitchOnTrusted : AppEvent() + + data object ToggleStopTunnelOnNoInternet : AppEvent() + + data object ToggleAutoTunnelWildcards : AppEvent() + + data object ToggleRootShellWifi : AppEvent() + + data class DeleteTrustedSSID(val ssid: String) : AppEvent() + + data class SaveTrustedSSID(val ssid: String) : AppEvent() + + data class ExportTunnels(val configType: ConfigType) : AppEvent() + + data object ExportLogs : AppEvent() + + data object DeleteLogs : AppEvent() + + data object MessageShown : AppEvent() + + data class ShowMessage(val message: StringValue) : AppEvent() + + data class ClearTunnelError(val tunnel: TunnelConf) : AppEvent() + + data class PopBackStack(val pop: Boolean) : AppEvent() + + data object ToggleBottomSheet : AppEvent() + + data class SetSelectedTunnel(val tunnel: TunnelConf?) : AppEvent() + + data object VpnPermissionRequested : AppEvent() + + data class AppReadyCheck(val tunnels: List) : AppEvent() + + data object ToggleRemoteControl : AppEvent() } diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 7d3da834..d93250a2 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -12,10 +12,9 @@ Název tunelu Vyloučit Zahrnout - Koncový bod + Koncový bod Povolit trvalé připojení VPN Služby polohy nebyly detekovány - Hledat balíčky VPN zapnuto VPN vypnuto Vytvořit od základu @@ -23,14 +22,12 @@ seznam oddělený čárkami (náhodné) (volitelné) - (volitelné, nedoporučeno) Předsdílený klíč vteřin(y) Zrušit Autentizace selhala Nepovedlo se autorizovat Zapnout zkratky - Exportovat konfigurace Došlo k neznámé chybě Tunelovat na nedůvěryhodné Wi-Fi WG Tunnel podpora @@ -38,8 +35,7 @@ Použít kernel modul SSID již existuje Oprávnění root zamítnuto - Žádný průzkumník souborů není nainstalován - Neplatný QR kód + Není nainstalován žádný průzkumník souborů Služba automatického tunelování Otevřít případ PIN je nesprávný @@ -62,8 +58,8 @@ Tunelovat na ethernetu Tato funkce vyžaduje oprávnění pro přístup k poloze na pozadí pro zapnutí monitorování Wi-Fi SSID, i když je aplikace zavřená. Pro více detailů, podívejte se prosím na zásady soukromí umístěné v kategorii Podpora. Tunely - Změny v konfiguraci uloženy. - Veřejný klíč + Změny v konfiguraci uloženy. + Veřejný klíč Adresy DNS servery Povolené IP adresy @@ -71,7 +67,7 @@ Automatické tunelování Akce vyžaduje aktivní tunel Rozhraní - Rotovat klíče + Rotovat klíče Soukromý klíč Kopírovat veřejný klíč base64 klíč @@ -81,9 +77,8 @@ Smazat tunel Jste si jisti, že chcete smazat tento tunel? Ano - Tunelování aplikací vše - Žádná emailová aplikace nebyla nalezena + Žádná emailová aplikace nebyla nalezena Žádný prohlížeč nebyl nalezen Přečíst si logy PIN úspěšně vytvořen @@ -95,7 +90,7 @@ Podpora WG Tunnel MTU - Naslouchací port + Naslouchací port (automaticky) Kernel Udržování spojení @@ -103,7 +98,7 @@ Zobrazit Amnezia možnosti Jazyk Světlé - Vzhled + Vzhled Téma zobrazení Tmavé Podle systému @@ -112,4 +107,60 @@ Název WiFi přes shell Použít root shell pro získání názvu WiFi Přidat ze schránky + Nastavit jako tunel pro ethernet + Povolit LAN provoz + Nastavit vlastní ping IP + Ping interval (s) + Logy + Automaticky + Hlavní tunel + Tunel pro mobilní data + Přeskočit + Kernel není podporován + Přispět na projekt + Zapnout místní protokolování + Zastavit tunel při ztrátě internetu + Zastavit při ztrátě internetu + Ethernet tunel + Nativní kill switch + VPN kill switch + Kanál pro oznámení auto-tunelu + Kill switch možnosti + Kanál pro oznámení o stavu VPN + Obejít LAN pro kill switch + Skrýt skripty + Rychlé akce + Skrýt Amnezia vlastnosti + Zapnout Amnezia kompatibilitu + Odebrat Amnezia kompatibilitu + Vyloučit LAN + Ovládání tunelu + Auto-tunel + Init packet magic header + Response packet magic header + Cooldown restartu pingu (s) + Důvěryhodné názvy wifi + Rozšířené nastavení + Zahrnout LAN + Přidat název wifi + Kanál pro oznámení VPN + Specifické nastavení tunelu + Zobrazit skripty + Dělené tunelování + Kanál pro oznámení o stavu auto-tunelu + Transport packet magic header + Underload packet magic header + Peer + Preferovat IPv4 připojení + Vypnout kill switch na důvěryhodných + Systémové nastavení VPN + nikdy + s + handshake + Spustit auto-tunel + Zastavit auto-tunel + Místní protokolování + Zjistit více + zastavit + Překlad názvu hostitele IPv4 diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 1feebd3b..ada3a97c 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -147,13 +147,11 @@ Fürs Projekt spenden Lokales Logging Lokales Logging aktivieren - Konfigurationsänderung Aus Zwischenablage einfügen Kill Switch Automatisch Sprache Anzeigetheme - Regeln für Tunnel bei Bedarf App Einstellungen aufrufen Primärer Tunnel Mobiler Daten-Tunnel @@ -200,9 +198,6 @@ Als WireGuard exportieren IPv4 Hostnamensauflösung IPv4 Verbindung bevorzugen - Fehler bei Endpunkt DNS Auflösung. Starten des Tunnels wegen Konfigfehler fehlgeschlagen. - Starten des Tunnels fehlgeschlagen, unberechtigt. - Tunnelfehler Mehrere diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 65de1e54..bbefd8af 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -104,15 +104,15 @@ seg Intervalo del Ping (seg) Encabezado del paquete de transporte - guía de inicio rápido + Guía de inicio Se ha denegado el permiso de conexión VPN. Por favor, compruebe el para asegurarse de que el VPN Siempre encendido esté desactivada para todas las demás aplicaciones e inténtelo de nuevo Encabezado del paquete de respuesta Tamaño máximo del paquete basura Tamaño basura del paquete de inicialización - si no está seguro de cómo continuar + Si no estás seguro de cómo proceder Ver la - Formato de configuración de túnel no válido + Configuración del formato del túnel no válida Permiso denegado Configuraciones VPN del sistema Unirse a la comunidad diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index b4404728..4447b814 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -3,4 +3,58 @@ WG Tunnel ファイルが.confまたは.zipではありません VPN通知チャンネル + QRコードから追加 + QRスキャン + VPNトンネル名 + 公開鍵 + 常時VPN接続を許可 + 位置情報サービスが検出されません + VPNオン + VPNオフ + インターフェース + 秘密鍵 + 公開鍵のコピー + カンマ区切りリスト + (オプション) + + 持続的キープアライブ + キャンセル + 認証に失敗しました + 不明なエラーが発生しました + ファイルエクスプローラーはインストールされていません + 認証に失敗しました + アプリのショートカットを有効にする + 設定のエクスポート + アプリがデバイスで有効になっている位置情報サービスを検出しません。 デバイスによっては、信頼されていない無線LAN機能がWiFi名を読み取れない可能性があります。このまま続けますか? + メールアプリは検出されません + メールを送る + ブラウザは検出されません + SSIDはすでに存在します + MTU + 実行にはトンネルオフが必要 + WGトンネルをご利用いただきありがとうございます! + ファイルまたはzipから追加する + ファイルを開く + DNSサーバー + (オプション、非推奨) + 事前共有鍵 + このトンネルを削除しますか? + はい + VPNトンネルはまだ追加されていません! + VPNトンネル + プライバシーポリシーを見る + この機能は、アプリケーションを閉じている間でもWi-Fi SSIDの監視を可能にするため、バックグラウンドでの位置情報の許可が必要です。 詳しくはサポート画面のプライバシーポリシーをご覧ください。 + メールを送る… + Read the docs + カーネルモジュールを使用する + 除外 + 含有 + 設定変更が保存された。 + ピア + 許可IPアドレス + エンドポイント + 実行に有効なVPNトンネルが必要です + ピアの追加 + 無効なQRコード + トンネルの削除 diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 637e065b..3bed1195 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -4,7 +4,7 @@ Actie vereist uitgeschakelde tunnel Tunnel bij mobiele data WG Tunnel - VPN Notificatiekanaal + VPN Notificatiekanaal Tunnels Bekijk privacybeleid Tunnel bij ethernet @@ -108,7 +108,7 @@ om zeker te zijn dat Altijd-aan VPN is uitgeschakeld voor alle andere apps, en probeer dan opnieuw (optioneel, standaard op peers) Ping herstart afkoelperiode (sec) - om te controleren of toegang is verleend. + om te controleren of toegang is verleend Amnezia instellingen weergeven nooit sec @@ -128,4 +128,12 @@ De app kan geen ingeschakelde locatieservices op je apparaat vinden. Afhankelijk van het type apparaat kan dit leiden tot een niet functionerende herkenning van het verbonden WiFi netwerk. De niet-vertrouwde WiFi functionaliteit werkt daardoor mogelijk niet. Toch doorgaan? Permanente achtergrondtoegang tot exacte locatie is vereist voor deze functie. Bekijk aub de Transport packet magic header + Kill-switch + Uiterlijk + Licht + Dynamisch + Donker + Taal + Automatisch + Meldingen diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 1b56e63e..2c35542c 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -201,7 +201,7 @@ Невозможно запустить туннель с ошибкой конфигурации. Невозможно запустить туннель без авторизации. Ошибка туннеля - Получение имени узла IPv4 + Получать имя узла IPv4 Несколько Экспортировать как Amnezia Экспортировать как WireGuard diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index a6b3daec..949b75a3 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -1,2 +1,208 @@ - \ No newline at end of file + + சுவிட்ச் விருப்பங்களைக் கொல்லுங்கள் + தனிப்பட்ட விசை + இந்த சுரங்கப்பாதையை நீக்க விரும்புகிறீர்களா? + QR குறியீட்டிலிருந்து சேர்க்கவும் + சுரங்கப்பாதை பயன்பாடுகள் + தவறான சுரங்கப்பாதை கட்டமைப்பு வடிவம் + Vpn on + விலக்கு + VPN மாநில அறிவிப்புகளுக்கான சேனல் + VPN அறிவிப்பு சேனல் + Wg சுரங்கப்பாதை + இன்னும் சுரங்கங்கள் எதுவும் சேர்க்கப்படவில்லை! + கோப்பு ஒரு .conf அல்லது .zip அல்ல + செயலுக்கு சுரங்கப்பாதை தேவை + சுரங்கங்கள் + மொபைல் தரவுகளில் சுரங்கப்பாதை + தனியுரிமைக் கொள்கையைக் காண்க + சரி + ஈத்தர்நெட்டில் சுரங்கப்பாதை + WG சுரங்கப்பாதையைப் பயன்படுத்தியதற்கு நன்றி! + SSID ஐ சமர்ப்பிக்கவும் + கோப்பு அல்லது சிப்பிலிருந்து சேர்க்கவும் + கோப்பு திறந்திருக்கும் + QR ச்கேன் + சுரங்கப்பாதை பெயர் + அடங்கும் + அனுமதிக்கப்பட்ட ஐபிக்கள் + முனைப்புள்ளி + பெயர் + ஆட்டோ-சுரங்கப்பாதை + Vpn அணை + புதிதாக உருவாக்கவும் + சகாக்களைச் சேர்க்கவும் + பொது விசையை நகலெடுக்கவும் + base64 விசை + கமா பிரிக்கப்பட்ட பட்டியல் + துறைமுகம் கேளுங்கள் + தொடர்ச்சியான கீப்அலிவ் + ரத்துசெய் + ஏற்பு தோல்வியடைந்தது + அங்கீகரிக்கத் தவறிவிட்டது + பயன்பாட்டு குறுக்குவழிகளை இயக்கவும் + ஏற்றுமதி உள்ளமைவுகள் + தெரியாத பிழை ஏற்பட்டது + நம்பத்தகாத வைஃபை மீது சுரங்கப்பாதை + WG சுரங்கப்பாதை உதவி + மின்னஞ்சல் அனுப்புங்கள்… + ஆவணத்தைப் படியுங்கள் + SSID ஏற்கனவே உள்ளது + ரூட் செல் மறுக்கப்பட்டது + கோப்பு எக்ச்ப்ளோரர் நிறுவப்படவில்லை + ஆம் + அனைத்தும் + மின்னஞ்சல் பயன்பாடு எதுவும் கண்டறியப்படவில்லை + உலாவி கண்டறியப்படவில்லை + ஒரு சிக்கலைத் திறக்கவும் + பதிவுகளைப் படியுங்கள் + (ஆட்டோ) + முள் தவறானது + முள் வெற்றிகரமாக உருவாக்கப்பட்டது + உங்கள் முள் உள்ளிடவும் + முள் உருவாக்கவும் + பயன்பாட்டு பூட்டை இயக்கவும் + பதிப்பு + அமைப்புகள் + உதவி + கர்னல் + குப்பை பாக்கெட் எண்ணிக்கை + குப்பை பாக்கெட் குறைந்தபட்ச அளவு + குப்பை பாக்கெட் அதிகபட்ச அளவு + Init பாக்கெட் குப்பை அளவு + மறுமொழி பாக்கெட் குப்பை அளவு + Init பாக்கெட் மேசிக் தலைப்பு + மறுமொழி பாக்கெட் மேசிக் தலைப்பு + போக்குவரத்து பாக்கெட் மேசிக் தலைப்பு + பாக்கெட் மேசிக் தலைப்பு + எப்படி தொடர வேண்டும் என்று உங்களுக்குத் தெரியாவிட்டால் + பார்க்க + தொடங்குதல் வழிகாட்டி + துவக்கத்தில் மறுதொடக்கம் செய்யுங்கள் + இசைவு மறுக்கப்பட்டது + VPN கணினி அமைப்புகள் + VPN இணைப்பு இசைவு மறுக்கப்பட்டுள்ளது. சரிபார்க்கவும் + மற்ற எல்லா பயன்பாடுகளுக்கும் எப்போதும் VPN அணைக்கப்பட்டு மீண்டும் முயற்சிக்கவும் என்பதை உறுதிப்படுத்திக் கொள்ளுங்கள் + இந்த அம்சத்திற்கு எல்லா நேர இருப்பிட இசைவு மற்றும்/அல்லது துல்லியமான இருப்பிடம் தேவை. தயவுசெய்து பார்க்கவும் + பயன்பாட்டு அமைப்புகள் + ரூட் செல் ஏற்றுக்கொள்ளப்பட்டது + தனிப்பயன் பிங் ஐபி அமைக்கவும் + (விரும்பினால், சகாக்களுக்கு இயல்புநிலை) + பிங் இடைவெளி (நொடி) + பிங் மறுதொடக்கம் கூல்டவுன் (நொடி) + அம்னேசியா பண்புகளைக் காட்டு + ஒருபோதும் + நொடி + ஏண்ட்சேக் + பதிவுகள் + சுவிட்சைக் கொல்லுங்கள் + தோற்றம் + அறிவிப்புகள் + தானியங்கி + ஒளி + இருண்ட + மொழி + கருப்பொருள் காட்சி + நம்பகமான வைஃபை பெயர்கள் + வைஃபை பெயரைச் சேர்க்கவும் + தேவை சுரங்கப்பாதை விதிகள் + முதன்மை சுரங்கப்பாதை + மொபைல் தரவு சுரங்கப்பாதை + தவிர் + பயன்பாட்டு அமைப்புகளைத் தொடங்கவும் + வைல்டு கார்டுகள் என்ற பெயரைப் பயன்படுத்தவும் + மேலும் அறிக + காடு கார்டுகள் செயலில் + செல் வழியாக வைஃபை பெயர் + வைஃபை பெயரைப் பெற ரூட் செல்லைப் பயன்படுத்தவும் + கர்னல் ஆதரிக்கப்படவில்லை + ஆட்டோ-டன்னல் தொடங்கவும் + ஆட்டோ-டன்னல் நிறுத்தவும் + சுரங்கப்பாதை இயங்கும் + மாநில மாற்றங்களை கண்காணித்தல் + திட்டத்திற்கு நன்கொடை + உள்ளூர்வாசிகள் + உள்ளக பதிவுகளை இயக்கவும் + உள்ளமைவு மாற்றம் + இந்த மாற்றத்திற்கு பயன்பாட்டு மறுதொடக்கம் தேவைப்படுகிறது. நீங்கள் தொடர விரும்புகிறீர்களா? + கிளிப்போர்டில் இருந்து சேர்க்கவும் + இணையத்தில் நிறுத்துங்கள் + இணைய இழப்பில் சுரங்கப்பாதையை நிறுத்துங்கள் + ஈத்தர்நெட் சுரங்கப்பாதை + ஈத்தர்நெட் சுரங்கப்பாதையாக அமைக்கவும் + சொந்த கொலை சுவிட்ச் + VPN கில் சுவிட்ச் + லேன் போக்குவரத்தை அனுமதிக்கவும் + கொலை சுவிட்சுக்கு பைபாச் லேன் + ஆட்டோ-டன்னல் அறிவிப்பு சேனல் + ஆட்டோ-டன்னல் மாநில அறிவிப்புகளுக்கான சேனல் + நிறுத்து + பிளவு சுரங்கப்பாதை + சுரங்கப்பாதை குறிப்பிட்ட அமைப்புகள் + ச்கிரிப்ட்களைக் காட்டு + முன் + இடுகை + முன் + இடுகை + கர்னல் பயன்முறையில் அம்னேசியா கிடைக்கவில்லை + அம்னேசியாவை இயக்கவும் + Wg பொருந்தக்கூடிய பயன்முறை + ச்கிரிப்ட்களை மறைக்கவும் + அம்னேசியா பொருந்தக்கூடிய தன்மையை இயக்கவும் + நேரந்தவறுகை + அம்னேசியா பண்புகளை மறைக்கவும் + அம்னேசியா பொருந்தக்கூடிய தன்மையை அகற்று + லானை விலக்கு + லேன் சேர்க்கவும் + சுரங்கப்பாதை தொடங்குவதில் தோல்வி + சுரங்கப்பாதை கட்டுப்பாடு + ஆட்டோ-டன்ரல் + பயன்பாடு மூடப்பட்டிருந்தாலும் கூட வைஃபை எச்எச்ஐடி கண்காணிப்பை இயக்க இந்த அம்சத்திற்கு பின்னணி இருப்பிட இசைவு தேவைப்படுகிறது. மேலும் விவரங்களுக்கு, உதவி திரையில் இணைக்கப்பட்ட தனியுரிமைக் கொள்கையைப் பார்க்கவும். + பின்னணி இருப்பிட வெளிப்பாடு + உள்ளமைவு மாற்றங்கள் சேமிக்கப்பட்டன. + பொது விசை + முகவரிகள் + டிஎன்எச் சேவையகங்கள் + மனிதன் + ஒப்பி + எப்போதும் VPN ஐ அனுமதிக்கவும் + இருப்பிட சேவைகள் கண்டறியப்படவில்லை + தொகுப்புகளைத் தேடுங்கள் + செயலுக்கு செயலில் சுரங்கப்பாதை தேவை + இடைமுகம் + விசைகளை சுழற்றுங்கள் + நொடிகள் + (சீரற்ற) + (விரும்பினால்) + (விரும்பினால், பரிந்துரைக்கப்படவில்லை) + முன் பகிரப்பட்ட விசை + எனக்கு மின்னஞ்சல் அனுப்புங்கள் + கர்னல் தொகுதியைப் பயன்படுத்தவும் + தவறான QR குறியீடு + உங்கள் சாதனத்தில் இயக்கப்பட்ட எந்த இருப்பிட சேவைகளையும் பயன்பாடு கண்டறியவில்லை. சாதனத்தைப் பொறுத்து, இது நம்பத்தகாத வைஃபை அம்சத்தை வைஃபை பெயரைப் படிக்கத் தவறிவிடும். எப்படியும் தொடர விரும்புகிறீர்களா? + ஆட்டோ-டன்னல் பணி + சுரங்கப்பாதையை நீக்கு + பிங் தோல்வியை மறுதொடக்கம் செய்யுங்கள் (பீட்டா) + மொபைல் தரவு சுரங்கப்பாதையாக அமைக்கவும் + முதன்மை சுரங்கப்பாதையாக அமைக்கவும் + வைஃபை பெயரில் சுரங்கப்பாதை பயன்படுத்தவும் + சுரங்கப்பாதை திருத்து + சமூகத்தில் சேரவும் + அம்சத்திற்கு குறைந்தது ஒரு சுரங்கப்பாதை தேவைப்படுகிறது + இந்த அனுமதிகள் இயக்கப்பட்டுள்ளன என்பதை உறுதிப்படுத்த + "விரும்பினால், இயல்புநிலை: " + மாறும் + விரைவான செயல்கள் + மேம்பட்ட அமைப்புகள் + எண்ட்பாயிண்ட் டி.என்.எச். + கட்டமைப்பு பிழையுடன் சுரங்கப்பாதையைத் தொடங்குவதில் தோல்வி. + சுரங்கப்பாதை தொடங்குவதில் தோல்வி, அங்கீகரிக்கப்படாதது. + சுரங்கப்பாதை தோல்வி + மறதி நோயாக ஏற்றுமதி + வயர் கார்டாக ஏற்றுமதி + ஐபிவி 4 ஓச்ட்பெயர் தீர்மானம் + ஐபிவி 4 இணைப்பை விரும்புங்கள் + நான்கு + நம்பகமானதைக் கொலை செய்வதை நிறுத்துங்கள் + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index bee4949c..01d93fb0 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -117,7 +117,7 @@ Час очікування перезапуску пінгу (сек.) Показати налаштування Amnezia сек. - рукостискання + з\'єднання Сповіщення Світла Темна diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml new file mode 100644 index 00000000..0d57895e --- /dev/null +++ b/app/src/main/res/values-ur/strings.xml @@ -0,0 +1,208 @@ + + + پیر + (اختیاری، تجویز کردہ نہیں) + QR کوڈ سے شامل کریں۔ + ہمیشہ آن VPN کی اجازت دیں۔ + ساتھی شامل کریں۔ + کارروائی کے لیے سرنگ بند کی ضرورت ہے۔ + چابیاں گھمائیں۔ + عوامی کلید کو کاپی کریں۔ + VPN نوٹیفکیشن چینل + (اختیاری) + کوما سے الگ کردہ فہرست + ایپ آپ کے آلے پر فعال مقام کی خدمات کا پتہ نہیں لگا رہی ہے۔ ڈیوائس پر منحصر ہے، یہ غیر بھروسہ مند وائی فائی فیچر کے وائی فائی کا نام پڑھنے میں ناکام ہونے کا سبب بن سکتا ہے۔ کیا آپ بہرحال جاری رکھنا چاہیں گے؟ + پس منظر کی جگہ کا انکشاف + عوامی کلید + ڈبلیو جی ٹنل + فائل conf یا zip نہیں ہے۔ + ابھی تک کوئی سرنگیں شامل نہیں کی گئیں! + سرنگیں۔ + موبائل ڈیٹا پر سرنگ + رازداری کی پالیسی دیکھیں + ٹھیک ہے۔ + ایتھرنیٹ پر سرنگ + ڈبلیو جی ٹنل استعمال کرنے کا شکریہ! + SSID جمع کروائیں۔ + فائل کھولیں۔ + کیو آر اسکین + ٹنل کا نام + خارج کرنا + شامل کریں۔ + پتے + DNS سرورز + ایم ٹی یو + اجازت یافتہ IPs + اختتامی نقطہ + نام + مقام کی خدمات کا پتہ نہیں چلا + پیکجز تلاش کریں۔ + VPN آن + VPN آف ہے۔ + کارروائی کے لیے فعال سرنگ کی ضرورت ہے۔ + انٹرفیس + نجی کلید + بیس 64 کلید + لسن پورٹ + (بے ترتیب) + پہلے سے مشترکہ کلید + منسوخ کریں۔ + تصدیق ناکام ہوگئی + اجازت دینے میں ناکام + ایپ شارٹ کٹس کو فعال کریں۔ + ترتیب برآمد کریں۔ + ناقابل اعتماد وائی فائی پر سرنگ + ڈبلیو جی ٹنل سپورٹ + دستاویزات پڑھیں + مجھے ایک ای میل بھیجیں۔ + کرنل ماڈیول استعمال کریں۔ + SSID پہلے سے موجود ہے۔ + روٹ شیل سے انکار کر دیا گیا۔ + کوئی فائل ایکسپلورر انسٹال نہیں ہے۔ + غلط QR کوڈ + آٹو ٹنل سروس + سرنگ کو حذف کریں۔ + کیا آپ واقعی اس سرنگ کو حذف کرنا چاہیں گے؟ + جی ہاں + ٹنلنگ ایپس + تمام + کوئی ای میل ایپ نہیں ملی + کوئی براؤزر نہیں ملا + ایک مسئلہ کھولیں۔ + نوشتہ جات پڑھیں + (خودکار) + پن غلط ہے۔ + پن کامیابی کے ساتھ بن گیا۔ + پن بنائیں + پنگ فیل پر دوبارہ شروع کریں (بی ٹا) + بنیادی سرنگ کے طور پر سیٹ کریں۔ + ورژن + ردی کے پیکٹ کی گنتی + Init پیکٹ ردی کا سائز + Init پیکٹ میجک ہیڈر + ٹرانسپورٹ پیکٹ میجک ہیڈر + دیکھیں + غلط سرنگ کنفیگریشن فارمیٹ + اجازت نامنظور + VPN سسٹم کی ترتیبات + VPN کنکشن کی اجازت مسترد کر دی گئی ہے۔ براہ کرم چیک کریں۔ + خصوصیت کے لیے کم از کم ایک سرنگ درکار ہے۔ + ایپ کی ترتیبات + روٹ شیل کو قبول کر لیا گیا۔ + اپنی مرضی کے مطابق پنگ آئی پی سیٹ کریں۔ + (اختیاری، ساتھیوں کے لیے ڈیفالٹس) + Amnezia کی خصوصیات دکھائیں۔ + سیکنڈ + ظاہری شکل + اطلاعات + خودکار + اندھیرا + متحرک + زبان + ڈسپلے تھیم + قابل اعتماد وائی فائی نام + بنیادی سرنگ + موبائل ڈیٹا ٹنل + چھوڑیں۔ + مزید جانیں + کرنل تعاون یافتہ نہیں ہے۔ + آٹو ٹنل شروع کریں۔ + پروجیکٹ کے لیے عطیہ کریں۔ + مقامی لاگنگ + مقامی لاگنگ کو فعال کریں۔ + بغیر انٹرنیٹ پر رکیں۔ + انٹرنیٹ کے نقصان پر سرنگ کو روکیں۔ + VPN کِل سوئچ + کِل سوئچ کے اختیارات + LAN ٹریفک کی اجازت دیں۔ + سپلٹ ٹنلنگ + ٹنل کی مخصوص ترتیبات + اسکرپٹ دکھائیں۔ + پری اپ + پوسٹ اپ + پری نیچے + پوسٹ نیچے + کرنل موڈ میں ایمنیزیا دستیاب نہیں ہے۔ + فوری اقدامات + اعلی درجے کی ترتیبات + ڈیباؤنس میں تاخیر + Amnezia کی خصوصیات کو چھپائیں۔ + LAN کو خارج کریں۔ + LAN شامل کریں۔ + سرنگ شروع کرنے میں ناکام + خودکار سرنگ + قابل اعتماد پر کِل سوئچ کو بند کریں۔ + IPv4 میزبان نام کی قرارداد + سرنگ شروع کرنے میں ناکام، غیر مجاز۔ + متعدد + Amnezia کے طور پر برآمد کریں۔ + اس فیچر کو ایپلیکیشن بند ہونے کے باوجود بھی Wi-Fi SSID مانیٹرنگ کو فعال کرنے کے لیے پس منظر کی جگہ کی اجازت درکار ہے۔ مزید تفصیلات کے لیے، براہ کرم سپورٹ اسکرین پر منسلک رازداری کی پالیسی دیکھیں۔ + ای میل بھیجیں… + فائل یا زپ سے شامل کریں۔ + کنفیگریشن تبدیلیاں محفوظ ہو گئیں۔ + خودکار سرنگ + شروع سے تخلیق کریں۔ + نامعلوم خرابی پیش آگئی + سیکنڈ + مسلسل زندہ رہنا + آن ڈیمانڈ ٹنل رولز + تشکیل کی خرابی کے ساتھ سرنگ شروع کرنے میں ناکام۔ + Amnezia مطابقت کو ہٹا دیں۔ + روشنی + ایتھرنیٹ ٹنل کے طور پر سیٹ کریں۔ + ایپ کی ترتیبات شروع کریں۔ + کمیونٹی میں شامل ہوں۔ + ریاستی تبدیلیوں کی نگرانی + کنفیگریشن تبدیلی + پنگ وقفہ (سیکنڈ) + شیل کے ذریعے وائی فائی کا نام + اپنا پن درج کریں۔ + شروع کرنے کی گائیڈ + پنگ دوبارہ شروع کول ڈاؤن (سیکنڈ) + مصافحہ + رسپانس پیکٹ میجک ہیڈر + اس تبدیلی کے لیے ایپ کو دوبارہ لانچ کرنے کی ضرورت ہے۔ کیا آپ آگے بڑھنا چاہیں گے؟ + اگر آپ کو یقین نہیں ہے کہ کیسے آگے بڑھیں۔ + وائی فائی نام پر ٹنل استعمال کریں۔ + "اختیاری، ڈیفالٹ: " + وائی فائی کا نام حاصل کرنے کے لیے روٹ شیل کا استعمال کریں۔ + رسپانس پیکٹ ردی کا سائز + جنک پیکٹ کا کم از کم سائز + موبائل ڈیٹا ٹنل کے طور پر سیٹ کریں۔ + سرنگ میں ترمیم کریں۔ + ایپ لاک کو فعال کریں۔ + نوشتہ جات + کرنل + بوٹ پر دوبارہ شروع کریں۔ + حمایت + یہ یقینی بنانے کے لیے کہ یہ اجازتیں فعال ہیں۔ + ترتیبات + جنک پیکٹ زیادہ سے زیادہ سائز + Amnezia مطابقت کو فعال کریں۔ + کِل سوئچ + IPv4 کنکشن کو ترجیح دیں۔ + مقامی قتل سوئچ + خودکار سرنگ ریاست کی اطلاعات کے لیے ایک چینل + WG مطابقت موڈ + نام وائلڈ کارڈ استعمال کریں۔ + آٹو ٹنل بند کرو + وائی فائی کا نام شامل کریں۔ + وائلڈ کارڈز فعال + کِل سوئچ کے لیے LAN کو بائی پاس کریں۔ + روکو + کبھی نہیں + پیکٹ میجک ہیڈر کو انڈر لوڈ کریں۔ + اس خصوصیت کے لیے ہر وقت مقام کی اجازت اور/یا درست مقام کی ضرورت ہے۔ ملاحظہ فرمائیں + یہ یقینی بنانے کے لیے کہ ہمیشہ آن VPN دیگر تمام ایپس کے لیے بند ہے اور دوبارہ کوشش کریں۔ + Amnezia کو فعال کریں۔ + ایتھرنیٹ سرنگ + آٹو ٹنل نوٹیفکیشن چینل + اختتامی نقطہ DNS کو حل کرنے میں ناکام۔ + وائر گارڈ کے بطور برآمد کریں۔ + ٹنل چل رہا ہے۔ + ٹنل کنٹرول + ٹنل کی ناکامی۔ + VPN ریاستی اطلاعات کے لیے ایک چینل + اسکرپٹ چھپائیں۔ + کلپ بورڈ سے شامل کریں۔ + diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index a2f508ee..ab79e3b6 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -98,4 +98,16 @@ 允許的 IP 最小無效封包 未安裝任何檔案管理器 + 如果您不確定如何進行 + 請參閱 + 查看日誌 + 本地日誌記錄 + 啟用本地日誌記錄 + 給作者寄電子郵件 + 允許永久連線的 VPN + 搜尋軟體套件 + VPN 已開啟 + VPN 已關閉 + 無法取得 root 權限 + 已取得 root 權限 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 81124901..734b7f14 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -39,7 +39,6 @@ Name Allow Always-On VPN Location Services Not Detected - Search packages wg-tunnel-db Auto-tunneling VPN on @@ -56,7 +55,6 @@ Listen port (random) (optional) - (optional, not recommended) Pre-shared key seconds Persistent keepalive @@ -64,7 +62,6 @@ Authentication failed Failed to authorize Enable app shortcuts - Export configs Unknown error occurred Tunnel on untrusted wifi support@zaneschepke.com @@ -76,14 +73,12 @@ SSID already exists Root shell denied No file explorer installed - Invalid QR code 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 - Tunneling apps - all + all No email app detected No browser detected Open an issue @@ -123,8 +118,6 @@ VPN system settings VPN connection permission has been denied. Please check the to make sure Always-on VPN is turned off for all other apps and try again - Join the community - Feature requires at least one tunnel Allow all the time location permission and/or precise location is required for this feature. Please see app settings to make sure these permissions are enabled @@ -150,8 +143,7 @@ Display theme Trusted wifi names Add wifi name - On demand tunnel rules - Primary tunnel + Primary tunnel Mobile data tunnel Skip Launch app settings @@ -168,9 +160,7 @@ Donate to project Local logging Enable local logging - Configuration change - This change requires an app relaunch. Would you like to proceed? - Add from clipboard + Add from clipboard Stop on no internet Stop tunnel on internet loss Ethernet tunnel @@ -192,10 +182,7 @@ Post up Pre down Post down - Amnezia unavailable in kernel mode - Enable Amnezia - WG compatibility mode - Quick actions + Quick actions Advanced settings Debounce delay Hide Amnezia properties @@ -204,23 +191,15 @@ Remove Amnezia compatibility Exclude LAN Include LAN - Failed to starting tunnel Tunnel control Auto-tunnel Stop kill switch on trusted IPv4 hostname resolution Prefer IPv4 connection - Failed to resolve endpoint DNS. - Failed to start tunnel with config error. - Failed to start tunnel, unauthorized. - Tunnel failure - Multiple - Export as Amnezia - Export as WireGuard + Multiple Add from URL Enter config URL Failed to download config - Invalid URL Save Search Select diff --git a/app/src/test/java/com/zaneschepke/wireguardautotunnel/ExampleUnitTest.kt b/app/src/test/java/com/zaneschepke/wireguardautotunnel/ExampleUnitTest.kt index 51bba8bc..435f6ba2 100644 --- a/app/src/test/java/com/zaneschepke/wireguardautotunnel/ExampleUnitTest.kt +++ b/app/src/test/java/com/zaneschepke/wireguardautotunnel/ExampleUnitTest.kt @@ -9,8 +9,8 @@ import org.junit.Test * See [testing documentation](http://d.android.com/tools/testing). */ class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } } diff --git a/build.gradle.kts b/build.gradle.kts index a84cf0cc..2d41f2e1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,20 +6,15 @@ plugins { alias(libs.plugins.ksp) apply false alias(libs.plugins.androidLibrary) apply false alias(libs.plugins.compose.compiler) apply false - alias(libs.plugins.ktlint) + alias(libs.plugins.ktfmt) } subprojects { apply { - plugin(rootProject.libs.plugins.ktlint.get().pluginId) + plugin(rootProject.libs.plugins.ktfmt.get().pluginId) } - ktlint { - debug.set(false) - verbose.set(true) - android.set(true) - outputToConsole.set(true) - ignoreFailures.set(false) - enableExperimentalRules.set(true) + ktfmt { + kotlinLangStyle() } } diff --git a/fastlane/metadata/android/cs-CZ/title.txt b/fastlane/metadata/android/cs-CZ/title.txt index 3743a7b3..e589556c 100644 --- a/fastlane/metadata/android/cs-CZ/title.txt +++ b/fastlane/metadata/android/cs-CZ/title.txt @@ -1 +1 @@ -WG Tunnel \ No newline at end of file +WG Tunnel diff --git a/fastlane/metadata/android/de-DE/changelogs/32100.txt b/fastlane/metadata/android/de-DE/changelogs/32100.txt deleted file mode 100644 index ea1cfd66..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/32100.txt +++ /dev/null @@ -1,3 +0,0 @@ -Verbesserungen: -- Berechtigungsfehler auf Android Versionen vor 9 behoben -- Andere Optimierungen diff --git a/fastlane/metadata/android/de-DE/changelogs/32200.txt b/fastlane/metadata/android/de-DE/changelogs/32200.txt deleted file mode 100644 index 9a330951..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/32200.txt +++ /dev/null @@ -1,5 +0,0 @@ -Verbesserungen: -- Tunnelstatistiken zum Hauptbildschirm hinzugefügt -- Navigation der Einstellungen auf AndroidTV verbessert -- Vibration von Benachrichtigungen entfernt -- Verschiedene andere Fehlerbehebungen diff --git a/fastlane/metadata/android/de-DE/changelogs/32300.txt b/fastlane/metadata/android/de-DE/changelogs/32300.txt deleted file mode 100644 index 64986e76..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/32300.txt +++ /dev/null @@ -1,5 +0,0 @@ -Verbesserungen: -- Unterstützung für Auto-Tunneln nur bei Verwendung von Mobilen Daten -- Verbesserungen der Support Oberfläche -- Aktualisierung der Ressourcenlinks -- Verschiedene andere Fehlerbehebungen diff --git a/fastlane/metadata/android/de-DE/changelogs/32400.txt b/fastlane/metadata/android/de-DE/changelogs/32400.txt deleted file mode 100644 index db453332..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/32400.txt +++ /dev/null @@ -1,5 +0,0 @@ -Verbesserungen: -- Grundlegende Unterstützung für WireGuard Kernelmodus hinzugefügt -- Verbesserung des Positionsanfrage -- Behebung eines Berechtigungsfehlers bei Auto-Tunneln -- Verschiedene andere GUI Fehlerbehebungen diff --git a/fastlane/metadata/android/de-DE/changelogs/32500.txt b/fastlane/metadata/android/de-DE/changelogs/32500.txt deleted file mode 100644 index a269b34b..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/32500.txt +++ /dev/null @@ -1,2 +0,0 @@ -Fehlerbehebungen: -- Android 14 Vordergrundberechtigungen diff --git a/fastlane/metadata/android/de-DE/changelogs/33000.txt b/fastlane/metadata/android/de-DE/changelogs/33000.txt deleted file mode 100644 index c55558be..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/33000.txt +++ /dev/null @@ -1,7 +0,0 @@ -Verbesserungen: -- Refaktorierung der Zustandsverwaltung -- Verbesserung der AndroidTV Navigation -- Verbesserung der Auto-Tunnel Effizienz -- Navigation verbessert -- Auto-Tunnel Pausefunktion -- Viele Fehlerbehebungen diff --git a/fastlane/metadata/android/de-DE/changelogs/33100.txt b/fastlane/metadata/android/de-DE/changelogs/33100.txt deleted file mode 100644 index 25ccb2e9..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/33100.txt +++ /dev/null @@ -1,7 +0,0 @@ -Verbesserungen: -- Refaktorisierung des Zustandsmanagements -- Verbesserung der AndroidTV Navigation -- Verbesserung der Auto-Tunnel Effizienz -- Verbesserung der Navigation -- Auto-Tunnel Pause Funktion -- Viele Fehlerbehebungen diff --git a/fastlane/metadata/android/de-DE/changelogs/33200.txt b/fastlane/metadata/android/de-DE/changelogs/33200.txt deleted file mode 100644 index 8f4f829b..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/33200.txt +++ /dev/null @@ -1,8 +0,0 @@ -Verbesserungen: -- Refaktorisierung des Zustandsmanagements -- Verbesserung der AndroidTV Navigation -- Verbesserung der Auto-Tunnel Effizienz -- Verbesserung der Navigation -- Auto-Tunnel Pause Funktion -- Reparatur des Auto-Tunnel starts im Vordergrund -- Viele Fehlerbehebungen diff --git a/fastlane/metadata/android/de-DE/changelogs/33300.txt b/fastlane/metadata/android/de-DE/changelogs/33300.txt deleted file mode 100644 index b79c0291..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/33300.txt +++ /dev/null @@ -1,7 +0,0 @@ -Verbesserungen: -- Bestätigung beim Tunnellöschen hinzugefügt -- Hintergrundberechtigung für Energiesparmodus hinzugefügt -Fehlerbehebungen: -- Einfrieren der App bei Tunneldeaktivierung -- Fehler im E-Mail Empfängerfeld -- Konfigurationsbearbeitung mit leerem DNS Feld diff --git a/fastlane/metadata/android/de-DE/changelogs/33400.txt b/fastlane/metadata/android/de-DE/changelogs/33400.txt deleted file mode 100644 index f3c25651..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/33400.txt +++ /dev/null @@ -1,3 +0,0 @@ -Verbesserungen: -- Erstellte Konfiguration wurde nicht gespeichert -- Version angehoben diff --git a/fastlane/metadata/android/de-DE/changelogs/33401.txt b/fastlane/metadata/android/de-DE/changelogs/33401.txt deleted file mode 100644 index 26db9d8b..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/33401.txt +++ /dev/null @@ -1,2 +0,0 @@ -Was ist neu: -- Dies ist eine CI Test version diff --git a/fastlane/metadata/android/de-DE/changelogs/33500.txt b/fastlane/metadata/android/de-DE/changelogs/33500.txt deleted file mode 100644 index 22cdde29..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/33500.txt +++ /dev/null @@ -1,5 +0,0 @@ -Was ist neu: -- Automatischer Start des Always-On VPN im Kernelmodus nach einem Systemneustart -- Unterstützung für adaptive Designicons -- Benachrichtigungs- und Kachelicons korrigiert -- AndroidTV icons korrigiert diff --git a/fastlane/metadata/android/de-DE/changelogs/33600.txt b/fastlane/metadata/android/de-DE/changelogs/33600.txt deleted file mode 100644 index 064d0aab..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/33600.txt +++ /dev/null @@ -1,5 +0,0 @@ -Was ist neu: -- Verbesserung des ersten Starts -- Wechsel zu Wireguard Bibliotheks Fork -- Abfrage der VPN Berechtigung beim ersten VPN Start -- Version angehoben diff --git a/fastlane/metadata/android/de-DE/changelogs/33700.txt b/fastlane/metadata/android/de-DE/changelogs/33700.txt deleted file mode 100644 index 2a7dfb7d..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/33700.txt +++ /dev/null @@ -1,2 +0,0 @@ -Was ist neu: -- GUI Fix for Tunnel Anzeige diff --git a/fastlane/metadata/android/de-DE/changelogs/33800.txt b/fastlane/metadata/android/de-DE/changelogs/33800.txt deleted file mode 100644 index cbea7e0a..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/33800.txt +++ /dev/null @@ -1,4 +0,0 @@ -Was ist neu: -- GUI Fix für Konfigurationseditor -- AOVPN Benachrichtung für ersten Start auf GrapheneOS hinzugefügt -- Version angehoben diff --git a/fastlane/metadata/android/de-DE/changelogs/33900.txt b/fastlane/metadata/android/de-DE/changelogs/33900.txt deleted file mode 100644 index 98136ee2..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/33900.txt +++ /dev/null @@ -1,5 +0,0 @@ -Was ist neu: -- Log anzeige hinzugefügt -- Lokale App Sperre hinzugefügt -- Funktion für Neustart des Tunnels nach Fehlerhaftem Ping -- Verschiedene Fehlerbehebungen diff --git a/fastlane/metadata/android/de-DE/changelogs/34000.txt b/fastlane/metadata/android/de-DE/changelogs/34000.txt deleted file mode 100644 index b643e638..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/34000.txt +++ /dev/null @@ -1,5 +0,0 @@ -Was ist neu: -- Auto-Tunnel Auswahl nach WLAN-Name -- Auto-Tunnel Steuerung durch Kacheln und Verknüpfungen -- Automatischer Neustart des Manuellen Tunnels nach einem Systemneustart -- Verschiedene Fehlerbehebungen und Performanceverbesserungen diff --git a/fastlane/metadata/android/de-DE/changelogs/34100.txt b/fastlane/metadata/android/de-DE/changelogs/34100.txt deleted file mode 100644 index 060d25b9..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/34100.txt +++ /dev/null @@ -1,5 +0,0 @@ -Was ist neu: -- Verbesserung der Auto-Tunnel Zuverlässigkeit -- Verbesserung der Kachelsynchronisierung -- AndroidTV Asset hinzugefügt -- APK Fingerabdruck hinzugefügt diff --git a/fastlane/metadata/android/de-DE/changelogs/34200.txt b/fastlane/metadata/android/de-DE/changelogs/34200.txt deleted file mode 100644 index a3f31ca0..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/34200.txt +++ /dev/null @@ -1,5 +0,0 @@ -Was ist neu: -- Behebung einer Regression beim Tunnel Stop -- Log Verschleierung hinzugefügt -- Ausblenden des Icons beim Scrollen -- Türkische Übersetzung hinzugefügt diff --git a/fastlane/metadata/android/de-DE/changelogs/34202.txt b/fastlane/metadata/android/de-DE/changelogs/34202.txt deleted file mode 100644 index ac860b65..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/34202.txt +++ /dev/null @@ -1,3 +0,0 @@ -Was ist neu: -- Amnezia seite-zu-seite mit WireGuard hinzugefügt -- App Shortcut Bug behoben diff --git a/fastlane/metadata/android/de-DE/changelogs/34300.txt b/fastlane/metadata/android/de-DE/changelogs/34300.txt deleted file mode 100644 index e77c35dc..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/34300.txt +++ /dev/null @@ -1,6 +0,0 @@ -Was ist neu: -- Offizielle Unterstützung für AmneziaWG -- Import/Export von Amnezia Konfigurationen -- Auto-Tunnel, um nur einmal pro Netzwerkänderung umzuschalten -- Unterstützung für zusätzliche Sprachen -- Weitere Fehlerbehebungen und Verbesserungen diff --git a/fastlane/metadata/android/de-DE/changelogs/34400.txt b/fastlane/metadata/android/de-DE/changelogs/34400.txt deleted file mode 100644 index f3e1e9a8..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/34400.txt +++ /dev/null @@ -1,5 +0,0 @@ -Was ist neu: -- Verbesserung der Benennung von Tunnel-Importen -- Fehler des Anfangszustand beim Auto-Tunneln behoben -- Verbesserte Fehlerhandhabung -- Fehler beim Import von Amnezia zip behoben diff --git a/fastlane/metadata/android/de-DE/changelogs/34500.txt b/fastlane/metadata/android/de-DE/changelogs/34500.txt deleted file mode 100644 index 0c83b344..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/34500.txt +++ /dev/null @@ -1,5 +0,0 @@ -Was ist neu -- Unterstützung für weitere Sprachen -- bug-fix für auto-tunneling bei mobilen Daten -- für AndroidTV: Bug bei schwebendem Aktionsknopf behoben -- andere Optimisation und Erweiterungen diff --git a/fastlane/metadata/android/de-DE/changelogs/34600.txt b/fastlane/metadata/android/de-DE/changelogs/34600.txt deleted file mode 100644 index 852e4be1..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/34600.txt +++ /dev/null @@ -1,4 +0,0 @@ -Was ist neu: -- Bug beim auto-tunneling behoben -- Bug bei Backups auf Android behoben -- Version angehoben diff --git a/fastlane/metadata/android/de-DE/changelogs/34700.txt b/fastlane/metadata/android/de-DE/changelogs/34700.txt deleted file mode 100644 index e988fb4e..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/34700.txt +++ /dev/null @@ -1,6 +0,0 @@ -Was ist Neu: -- Abstürze behoben -- Leistung der Kacheln verbessert -- Reaktivieren von PIN-Sperren -- "Beim Hochfahren starten" zur Einstellung gemacht -- verschiedene Leistungsanpassungen und Bugs behoben diff --git a/fastlane/metadata/android/de-DE/changelogs/34800.txt b/fastlane/metadata/android/de-DE/changelogs/34800.txt deleted file mode 100644 index 2e996d6d..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/34800.txt +++ /dev/null @@ -1,5 +0,0 @@ -Was ist neu: -- Verbesserungen der Tunnelsteuerung in der AndroidTV Benutzeroberfläche -- Fehler beim Sperren im Portrait-Modus behoben -- Fehler welcher eine Umgehung der PIN-Sperre ermöglichte behoben -- Fehler in der Auot-Tunnel Kachel behoben diff --git a/fastlane/metadata/android/de-DE/changelogs/35000.txt b/fastlane/metadata/android/de-DE/changelogs/35000.txt deleted file mode 100644 index c6e95ff7..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/35000.txt +++ /dev/null @@ -1,6 +0,0 @@ -Was ist neu? -- Verbesserte Zuverlässigkeit des automatischen Tunnels -- Unterstützung für hell/dunkel/dynamisches Theming hinzugefügt -- Unterstützung für Pre-/Post-Up-/Down-Skripte hinzugefügt -- Entfernen der Benachrichtigung über das Fortbestehen des Tunnels -- Verschiedene andere Korrekturen und Verbesserungen diff --git a/fastlane/metadata/android/de-DE/changelogs/35100.txt b/fastlane/metadata/android/de-DE/changelogs/35100.txt deleted file mode 100644 index aa40954d..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/35100.txt +++ /dev/null @@ -1,5 +0,0 @@ -Was ist neu? -- Behebung von Problemen beim Starten von Tunneln im Hintergrund -- Unterstützung für den Neustart von Diensten nach dem Update hinzugefügt -- Verbesserungen der UI-Animationsgeschwindigkeit -- Andere Optimierungen diff --git a/fastlane/metadata/android/de-DE/changelogs/35200.txt b/fastlane/metadata/android/de-DE/changelogs/35200.txt deleted file mode 100644 index abdb988b..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/35200.txt +++ /dev/null @@ -1,6 +0,0 @@ -Neu: -- Wildcard-Unterstützung für Wifi-Namen hinzugefügt -- Live-Bearbeitung von Einstellungen von aktiven Tunneln/Auto-Tunneln -- Niedrige Geschwindigkeit bei mobilen Daten repariert -- Verschiedene Fehlerbehebungen und Verbesserungen -- UI-Optimierungen diff --git a/fastlane/metadata/android/de-DE/changelogs/35300.txt b/fastlane/metadata/android/de-DE/changelogs/35300.txt deleted file mode 100644 index 84b32516..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/35300.txt +++ /dev/null @@ -1,6 +0,0 @@ -Was ist neu? -- Gerootete Geräte erhalten jetzt den WiFi-Namen ohne Standort -- Verbesserungen beim Scrollen und Teilen von Protokollen -- Fehlerbehebungen beim Tunnelimport für AndroidTV 14 -- Verbesserungen der Tunnel-Statistik UI -- Andere Fehlerbehebungen und Verbesserungen diff --git a/fastlane/metadata/android/de-DE/changelogs/36000.txt b/fastlane/metadata/android/de-DE/changelogs/36000.txt deleted file mode 100644 index dd24c136..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/36000.txt +++ /dev/null @@ -1,6 +0,0 @@ -Was ist neu? -- UI-Aktualisierung -- Verbesserungen der AndroidTV-Navigation -- Behebung eines Fehlers, der den Akku entleert -- Fehlerbehebung bei Wildcards mit optionaler Einstellung -- Weitere Verbesserungen diff --git a/fastlane/metadata/android/de-DE/changelogs/36100.txt b/fastlane/metadata/android/de-DE/changelogs/36100.txt deleted file mode 100644 index 0f378c6b..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/36100.txt +++ /dev/null @@ -1,7 +0,0 @@ -Was ist neu? -- Tunnel aus der Zwischenablage hinzufügen -- Lokalisierungen hinzugefügt -- Behebung eines Fehlers, der den Akku entleert -- Fehler beim Löschen behoben -- Verbesserte Synchronisation der Tunnelkacheln -- Andere Korrekturen und Verbesserungen diff --git a/fastlane/metadata/android/de-DE/changelogs/36200.txt b/fastlane/metadata/android/de-DE/changelogs/36200.txt deleted file mode 100644 index 493dba27..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/36200.txt +++ /dev/null @@ -1,6 +0,0 @@ -Was ist neu? -- VPN-Notschalter mit LAN-Bypass hinzugefügt -- Verbesserte automatische Tunnelgeschwindigkeit und Zuverlässigkeit - -- Verbesserte Kachelsynchronisation -- Verschiedene Fehlerbehebungen und Verbesserungen diff --git a/fastlane/metadata/android/de-DE/changelogs/36300.txt b/fastlane/metadata/android/de-DE/changelogs/36300.txt deleted file mode 100644 index bd383f2e..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/36300.txt +++ /dev/null @@ -1,3 +0,0 @@ -Was ist neu? -- Fehler beim Umschalten des Kernelmodus behoben -- Fehler beim Absturz der Benachrichtigung behoben diff --git a/fastlane/metadata/android/de-DE/changelogs/36500.txt b/fastlane/metadata/android/de-DE/changelogs/36500.txt deleted file mode 100644 index d2744ddd..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/36500.txt +++ /dev/null @@ -1,6 +0,0 @@ -Was ist neu? -- Verbesserungen der Zuverlässigkeit des automatischen Tunnels -- Behebung von Problemen beim Speichern von geteilten Tunneln und Leistungsverbesserungen -- Kernel-Modus-Fehler behoben -- Kachel-Fehler behoben -- Verschiedene andere Korrekturen und Verbesserungen diff --git a/fastlane/metadata/android/de-DE/changelogs/36600.txt b/fastlane/metadata/android/de-DE/changelogs/36600.txt deleted file mode 100644 index 98f7ab48..00000000 --- a/fastlane/metadata/android/de-DE/changelogs/36600.txt +++ /dev/null @@ -1,6 +0,0 @@ -Was ist neu? -- Die Ping-Funktion funktioniert jetzt unabhängig vom automatischen Tunnel -- Komfortaktion für Amnezia-Kompatibilität hinzugefügt -- Komfortaktion zum Ausschließen von LAN aus dem Tunnel hinzugefügt -- Option zur Einstellung der Entprellungszeit für Autotunnel hinzugefügt -- Viele Fehlerkorrekturen und Verbesserungen diff --git a/fastlane/metadata/android/en-US/changelogs/32100.txt b/fastlane/metadata/android/en-US/changelogs/32100.txt deleted file mode 100644 index 1a748590..00000000 --- a/fastlane/metadata/android/en-US/changelogs/32100.txt +++ /dev/null @@ -1,3 +0,0 @@ -Enhancements: -- Fix < Android 9 permission bug -- Other optimizations diff --git a/fastlane/metadata/android/en-US/changelogs/32200.txt b/fastlane/metadata/android/en-US/changelogs/32200.txt deleted file mode 100644 index ec75373c..00000000 --- a/fastlane/metadata/android/en-US/changelogs/32200.txt +++ /dev/null @@ -1,5 +0,0 @@ -Enhancements: -- Add tunnel statistics to main screen -- Improve settings screen AndroidTV navigation -- Remove notification vibration -- Various other bug fixes \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/32300.txt b/fastlane/metadata/android/en-US/changelogs/32300.txt deleted file mode 100644 index d3687e1d..00000000 --- a/fastlane/metadata/android/en-US/changelogs/32300.txt +++ /dev/null @@ -1,5 +0,0 @@ -Enhancements: -- Add support for mobile data only auto-tunneling -- Improve support screen UI -- Update resource links -- Various other bug fixes \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/32400.txt b/fastlane/metadata/android/en-US/changelogs/32400.txt deleted file mode 100644 index 13b80843..00000000 --- a/fastlane/metadata/android/en-US/changelogs/32400.txt +++ /dev/null @@ -1,5 +0,0 @@ -Enhancements: -- Add basic WireGuard Kernel support -- Improved location disclosure flow -- Fix auto-tunnel permissions bug -- Various other UI bug fixes \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/32500.txt b/fastlane/metadata/android/en-US/changelogs/32500.txt deleted file mode 100644 index 10f009cf..00000000 --- a/fastlane/metadata/android/en-US/changelogs/32500.txt +++ /dev/null @@ -1,2 +0,0 @@ -Fixes: -- Android 14 foreground permissions \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/33000.txt b/fastlane/metadata/android/en-US/changelogs/33000.txt deleted file mode 100644 index c75588b6..00000000 --- a/fastlane/metadata/android/en-US/changelogs/33000.txt +++ /dev/null @@ -1,8 +0,0 @@ -Enhancements: -- Refactor state management -- Improve AndroidTV navigation -- Improve auto-tunneling efficiency -- Improve navigation -- Auto-tunneling pause feature -- Many bugfixes - diff --git a/fastlane/metadata/android/en-US/changelogs/33100.txt b/fastlane/metadata/android/en-US/changelogs/33100.txt deleted file mode 100644 index b5c910fb..00000000 --- a/fastlane/metadata/android/en-US/changelogs/33100.txt +++ /dev/null @@ -1,7 +0,0 @@ -Enhancements: -- Refactor state management -- Improve AndroidTV navigation -- Improve auto-tunneling efficiency -- Improve navigation -- Auto-tunneling pause feature -- Many bugfixes diff --git a/fastlane/metadata/android/en-US/changelogs/33200.txt b/fastlane/metadata/android/en-US/changelogs/33200.txt deleted file mode 100644 index 0d36a349..00000000 --- a/fastlane/metadata/android/en-US/changelogs/33200.txt +++ /dev/null @@ -1,8 +0,0 @@ -Enhancements: -- Refactor state management -- Improve AndroidTV navigation -- Improve auto-tunneling efficiency -- Improve navigation -- Auto-tunneling pause feature -- Fix foreground start auto-tunnel -- Many bugfixes diff --git a/fastlane/metadata/android/en-US/changelogs/33300.txt b/fastlane/metadata/android/en-US/changelogs/33300.txt deleted file mode 100644 index 9278b40b..00000000 --- a/fastlane/metadata/android/en-US/changelogs/33300.txt +++ /dev/null @@ -1,7 +0,0 @@ -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/fastlane/metadata/android/en-US/changelogs/33400.txt b/fastlane/metadata/android/en-US/changelogs/33400.txt deleted file mode 100644 index 006e9869..00000000 --- a/fastlane/metadata/android/en-US/changelogs/33400.txt +++ /dev/null @@ -1,3 +0,0 @@ -Enhancements: -- Fixed create config not saving -- Bumped versions \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/33401.txt b/fastlane/metadata/android/en-US/changelogs/33401.txt deleted file mode 100644 index 044f1442..00000000 --- a/fastlane/metadata/android/en-US/changelogs/33401.txt +++ /dev/null @@ -1,2 +0,0 @@ -What's new: -- This is a CI testing version \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/33500.txt b/fastlane/metadata/android/en-US/changelogs/33500.txt deleted file mode 100644 index 33954f06..00000000 --- a/fastlane/metadata/android/en-US/changelogs/33500.txt +++ /dev/null @@ -1,6 +0,0 @@ -What's new: -- Auto-start on reboot for Always-On VPN kernel mode -- Support for adaptive theme icons -- Fix notification icons, tile icon -- Fix AndroidTV icons - diff --git a/fastlane/metadata/android/en-US/changelogs/33600.txt b/fastlane/metadata/android/en-US/changelogs/33600.txt deleted file mode 100644 index 6f421552..00000000 --- a/fastlane/metadata/android/en-US/changelogs/33600.txt +++ /dev/null @@ -1,5 +0,0 @@ -What's new: -- Improve first launch flow -- Switch to wireguard lib fork -- Request VPN permission on first VPN start -- Bump versions \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/33700.txt b/fastlane/metadata/android/en-US/changelogs/33700.txt deleted file mode 100644 index b9819ecf..00000000 --- a/fastlane/metadata/android/en-US/changelogs/33700.txt +++ /dev/null @@ -1,2 +0,0 @@ -What's new: -- Tunnel display UI bug fix \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/33800.txt b/fastlane/metadata/android/en-US/changelogs/33800.txt deleted file mode 100644 index c92a8b7e..00000000 --- a/fastlane/metadata/android/en-US/changelogs/33800.txt +++ /dev/null @@ -1,4 +0,0 @@ -What's new: -- Config edit UI bug fix -- Add GrapheneOS first launch AOVPN notification -- Bump versions diff --git a/fastlane/metadata/android/en-US/changelogs/33900.txt b/fastlane/metadata/android/en-US/changelogs/33900.txt deleted file mode 100644 index 43e104ec..00000000 --- a/fastlane/metadata/android/en-US/changelogs/33900.txt +++ /dev/null @@ -1,5 +0,0 @@ -What's new: -- Add logs screen -- Add local app lock -- Add restart vpn on failed ping -- Various bug fixes \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/34000.txt b/fastlane/metadata/android/en-US/changelogs/34000.txt deleted file mode 100644 index 8a2a1ba4..00000000 --- a/fastlane/metadata/android/en-US/changelogs/34000.txt +++ /dev/null @@ -1,6 +0,0 @@ -What's new: -- Auto tunnel to specific tunnels by wifi name -- Auto tunnel control from tile and shortcuts -- Auto start manual tunnel if it was on before reboot -- Various bug fixes and performance improvements - diff --git a/fastlane/metadata/android/en-US/changelogs/34100.txt b/fastlane/metadata/android/en-US/changelogs/34100.txt deleted file mode 100644 index d940104b..00000000 --- a/fastlane/metadata/android/en-US/changelogs/34100.txt +++ /dev/null @@ -1,5 +0,0 @@ -What's new: -- Improved auto tunnel reliability -- Improved tile sync -- Added AndroidTV assets -- Added apk fingerprint diff --git a/fastlane/metadata/android/en-US/changelogs/34200.txt b/fastlane/metadata/android/en-US/changelogs/34200.txt deleted file mode 100644 index 5d97cf0a..00000000 --- a/fastlane/metadata/android/en-US/changelogs/34200.txt +++ /dev/null @@ -1,5 +0,0 @@ -What's new: -- Fix stop tunnel regression -- Add logs obfuscation -- Add hide FAB on scroll -- Add Turkish localization \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/34202.txt b/fastlane/metadata/android/en-US/changelogs/34202.txt deleted file mode 100644 index fcbdc084..00000000 --- a/fastlane/metadata/android/en-US/changelogs/34202.txt +++ /dev/null @@ -1,3 +0,0 @@ -What's new: -- Add Amnezia side-by-side with WireGuard -- Fix app shortcuts bug \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/34300.txt b/fastlane/metadata/android/en-US/changelogs/34300.txt deleted file mode 100644 index 2b28f610..00000000 --- a/fastlane/metadata/android/en-US/changelogs/34300.txt +++ /dev/null @@ -1,6 +0,0 @@ -What's new: -- Official support for AmneziaWG -- Import/export for Amnezia configs -- Auto-tunnel to only toggle once per network change -- Additional languages support -- Other bug fixes and improvements \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/34400.txt b/fastlane/metadata/android/en-US/changelogs/34400.txt deleted file mode 100644 index f796bf84..00000000 --- a/fastlane/metadata/android/en-US/changelogs/34400.txt +++ /dev/null @@ -1,5 +0,0 @@ -What's new: -- Improve tunnel import naming -- Fix auto tunneling init state bug -- Improved error handling -- Fix Amnezia zip import bug \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/34500.txt b/fastlane/metadata/android/en-US/changelogs/34500.txt deleted file mode 100644 index f463fcff..00000000 --- a/fastlane/metadata/android/en-US/changelogs/34500.txt +++ /dev/null @@ -1,5 +0,0 @@ -What's new: -- Additional language support -- Auto-tunneling mobile data bug fix -- AndroidTV floating action button fix -- Other optimizations and enhancements \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/34600.txt b/fastlane/metadata/android/en-US/changelogs/34600.txt deleted file mode 100644 index 6666ca4b..00000000 --- a/fastlane/metadata/android/en-US/changelogs/34600.txt +++ /dev/null @@ -1,4 +0,0 @@ -What's new: -- Fixes auto tunneling bugs -- Fixes android backup bug -- Bump versions \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/34700.txt b/fastlane/metadata/android/en-US/changelogs/34700.txt deleted file mode 100644 index b74851cb..00000000 --- a/fastlane/metadata/android/en-US/changelogs/34700.txt +++ /dev/null @@ -1,6 +0,0 @@ -What's new: -- Fix crashing issues -- Improve tile performance -- Re-enable pin lock -- Make restart on boot a setting -- Various performance and bug fixes \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/34800.txt b/fastlane/metadata/android/en-US/changelogs/34800.txt deleted file mode 100644 index dff7fa4a..00000000 --- a/fastlane/metadata/android/en-US/changelogs/34800.txt +++ /dev/null @@ -1,5 +0,0 @@ -What's new: -- Fixes for AndroidTV UI tunnel control -- Fixes portrait lock bug -- Fixes pin lock bypass bug -- Fixes auto tunnel tile bug \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/35000.txt b/fastlane/metadata/android/en-US/changelogs/35000.txt deleted file mode 100644 index dd93c127..00000000 --- a/fastlane/metadata/android/en-US/changelogs/35000.txt +++ /dev/null @@ -1,6 +0,0 @@ -What's new: -- Improve auto tunnel reliability -- Add support for light/dark/dynamic theming -- Add support for pre/post up/down scripts -- Remove tunnel persistent notification -- Various other fixes and improvements diff --git a/fastlane/metadata/android/en-US/changelogs/35100.txt b/fastlane/metadata/android/en-US/changelogs/35100.txt deleted file mode 100644 index 02166614..00000000 --- a/fastlane/metadata/android/en-US/changelogs/35100.txt +++ /dev/null @@ -1,5 +0,0 @@ -What's new: -- Fixes for tunnels not launching from background -- Add support for restart services after update -- UI animation speed improvements -- Other optimizations diff --git a/fastlane/metadata/android/en-US/changelogs/35200.txt b/fastlane/metadata/android/en-US/changelogs/35200.txt deleted file mode 100644 index b7f10227..00000000 --- a/fastlane/metadata/android/en-US/changelogs/35200.txt +++ /dev/null @@ -1,6 +0,0 @@ -What's new: -- Added wildcard support for wifi names -- Live edit of tunnel/auto-tunnel settings while active -- Fix slowness on mobile data -- Various bug fixes and improvements -- UI optimizations diff --git a/fastlane/metadata/android/en-US/changelogs/35300.txt b/fastlane/metadata/android/en-US/changelogs/35300.txt deleted file mode 100644 index 380dc0e9..00000000 --- a/fastlane/metadata/android/en-US/changelogs/35300.txt +++ /dev/null @@ -1,6 +0,0 @@ -What's new: -- Rooted devices now get wifi name without location -- Logs screen scroll and sharing improvements -- Tunnel import bug fixes for AndroidTV 14 -- Tunnel statistics UI enhancements -- Other bug fixes and improvements diff --git a/fastlane/metadata/android/en-US/changelogs/36000.txt b/fastlane/metadata/android/en-US/changelogs/36000.txt deleted file mode 100644 index 4289062e..00000000 --- a/fastlane/metadata/android/en-US/changelogs/36000.txt +++ /dev/null @@ -1,6 +0,0 @@ -What's new: -- UI update -- AndroidTV navigation improvements -- Battery drain bug fix -- Wildcards fix with optional setting -- Other improvements diff --git a/fastlane/metadata/android/en-US/changelogs/36100.txt b/fastlane/metadata/android/en-US/changelogs/36100.txt deleted file mode 100644 index 4ca42428..00000000 --- a/fastlane/metadata/android/en-US/changelogs/36100.txt +++ /dev/null @@ -1,7 +0,0 @@ -What's new: -- Add tunnel from clipboard -- Add localizations -- Fix battery drain bug -- Fix delete bug -- Improve tunnel tile sync -- Other fixes and improvements diff --git a/fastlane/metadata/android/en-US/changelogs/36200.txt b/fastlane/metadata/android/en-US/changelogs/36200.txt deleted file mode 100644 index 31581fe4..00000000 --- a/fastlane/metadata/android/en-US/changelogs/36200.txt +++ /dev/null @@ -1,5 +0,0 @@ -What's new: -- Added VPN kill switch w/LAN bypass -- Improved auto tunnel speed and reliability -- Improved tile sync -- Various bug fixes and improvements diff --git a/fastlane/metadata/android/en-US/changelogs/36300.txt b/fastlane/metadata/android/en-US/changelogs/36300.txt deleted file mode 100644 index bee64fe9..00000000 --- a/fastlane/metadata/android/en-US/changelogs/36300.txt +++ /dev/null @@ -1,3 +0,0 @@ -What's new: -- Fixed kernel mode toggle bug -- Fixed notification crash bug diff --git a/fastlane/metadata/android/en-US/changelogs/36500.txt b/fastlane/metadata/android/en-US/changelogs/36500.txt deleted file mode 100644 index f48465bd..00000000 --- a/fastlane/metadata/android/en-US/changelogs/36500.txt +++ /dev/null @@ -1,6 +0,0 @@ -What's new: -- Auto tunnel reliability improvements -- Split tunnel save fix and performance improvements -- Kernel mode bug fix -- Tile bug fix -- Various other fixes and improvements diff --git a/fastlane/metadata/android/en-US/changelogs/36600.txt b/fastlane/metadata/android/en-US/changelogs/36600.txt deleted file mode 100644 index 934bb29a..00000000 --- a/fastlane/metadata/android/en-US/changelogs/36600.txt +++ /dev/null @@ -1,6 +0,0 @@ -What's new: -- Ping feature now works independent of auto tunnel -- Added convenience action for Amnezia compatibility -- Added convenience action for excluding LAN from tunnel -- Added debounce delay tuning option for auto tunnel -- Many bug fixes and improvements diff --git a/fastlane/metadata/android/es-ES/changelogs/32100.txt b/fastlane/metadata/android/es-ES/changelogs/32100.txt deleted file mode 100644 index 7bc7e39e..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/32100.txt +++ /dev/null @@ -1,3 +0,0 @@ -Mejoras: -- Corrección de fallo de permisos en < Android 9 -- Otras optimizaciones diff --git a/fastlane/metadata/android/es-ES/changelogs/32200.txt b/fastlane/metadata/android/es-ES/changelogs/32200.txt deleted file mode 100644 index 6d350d4f..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/32200.txt +++ /dev/null @@ -1,5 +0,0 @@ -Mejoras: -- Añadidas estadísticas del túnel a la pantalla principal -- Navegación mejorada en la pantalla de ajustes para AndroidTV -- Vibración de notificación quitada -- Otras correcciones diff --git a/fastlane/metadata/android/es-ES/changelogs/32300.txt b/fastlane/metadata/android/es-ES/changelogs/32300.txt deleted file mode 100644 index 5aac5a69..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/32300.txt +++ /dev/null @@ -1,5 +0,0 @@ -Mejoras: -- Compatibilidad de túnel-automático cuando solo para datos móviles -- UI de la pantalla de ayuda mojorada -- Enlaces de recursos mejorados -- Otras correcciones diff --git a/fastlane/metadata/android/es-ES/changelogs/32400.txt b/fastlane/metadata/android/es-ES/changelogs/32400.txt deleted file mode 100644 index 39ab5293..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/32400.txt +++ /dev/null @@ -1,5 +0,0 @@ -Mejoras: -- Añadida compatiblidad básica de Kernel WireGuard -- Flujo de divulgación de ubicación mejorada -- Corregido el fallo ede los persmisos del túnel automático -- Correcciones y mejoras varias diff --git a/fastlane/metadata/android/es-ES/changelogs/32500.txt b/fastlane/metadata/android/es-ES/changelogs/32500.txt deleted file mode 100644 index 02bbf843..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/32500.txt +++ /dev/null @@ -1,2 +0,0 @@ -Correcciones: -- Permisos en primer plano en Android 14 diff --git a/fastlane/metadata/android/es-ES/changelogs/33000.txt b/fastlane/metadata/android/es-ES/changelogs/33000.txt deleted file mode 100644 index 385ce05a..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/33000.txt +++ /dev/null @@ -1,7 +0,0 @@ -Mejoras: -- Gestión del estado de refactorización. -- Mejorar la navegación de AndroidTV -- Mejorar la eficiencia del túnel automático -- Mejorar la navegación -- Función de pausa de túnel automático -- Muchas correcciones de errores diff --git a/fastlane/metadata/android/es-ES/changelogs/33100.txt b/fastlane/metadata/android/es-ES/changelogs/33100.txt deleted file mode 100644 index 385ce05a..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/33100.txt +++ /dev/null @@ -1,7 +0,0 @@ -Mejoras: -- Gestión del estado de refactorización. -- Mejorar la navegación de AndroidTV -- Mejorar la eficiencia del túnel automático -- Mejorar la navegación -- Función de pausa de túnel automático -- Muchas correcciones de errores diff --git a/fastlane/metadata/android/es-ES/changelogs/33200.txt b/fastlane/metadata/android/es-ES/changelogs/33200.txt deleted file mode 100644 index 4d6c8722..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/33200.txt +++ /dev/null @@ -1,8 +0,0 @@ -Mejoras: -- Gestión del estado de refactorización. -- Mejorar la navegación de AndroidTV -- Mejorar la eficiencia del túnel automático -- Mejorar la navegación -- Función de pausa de túnel automático -- Se corrigió el inicio del túnel automático en primer plano. -- Muchas correcciones de errores diff --git a/fastlane/metadata/android/es-ES/changelogs/33300.txt b/fastlane/metadata/android/es-ES/changelogs/33300.txt deleted file mode 100644 index 6ec88f52..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/33300.txt +++ /dev/null @@ -1,7 +0,0 @@ -Mejoras: -- Se agregó confirmación al eliminar el túnel. -- Se agregó permiso en segundo plano para el modo de ahorro de energía. -Corrección de errores: -- La aplicación se congela cuando el túnel está desactivado. -- Error en el campo del destinatario del correo electrónico -- Edición de configuración con campo DNS vacío diff --git a/fastlane/metadata/android/es-ES/changelogs/33400.txt b/fastlane/metadata/android/es-ES/changelogs/33400.txt deleted file mode 100644 index b67d32e5..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/33400.txt +++ /dev/null @@ -1,3 +0,0 @@ -Mejoras: -- La configuración creada no se guardó. -- Versión elevada diff --git a/fastlane/metadata/android/es-ES/changelogs/33401.txt b/fastlane/metadata/android/es-ES/changelogs/33401.txt deleted file mode 100644 index 89670397..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/33401.txt +++ /dev/null @@ -1,2 +0,0 @@ -Qué hay de nuevo: -- Esta es una versión de prueba de CI diff --git a/fastlane/metadata/android/es-ES/changelogs/33500.txt b/fastlane/metadata/android/es-ES/changelogs/33500.txt deleted file mode 100644 index 4f723b49..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/33500.txt +++ /dev/null @@ -1,5 +0,0 @@ -Qué hay de nuevo: -- Inicio automático al reiniciar para el modo kernel de VPN siempre activo -- Soporte para íconos de temas adaptables -- Arreglar íconos de notificación, ícono de mosaico -- Reparar iconos de AndroidTV diff --git a/fastlane/metadata/android/es-ES/changelogs/33600.txt b/fastlane/metadata/android/es-ES/changelogs/33600.txt deleted file mode 100644 index c54a9019..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/33600.txt +++ /dev/null @@ -1,5 +0,0 @@ -¿Qué hay de nuevo?: -- Mejorar el flujo del primer lanzamiento. -- Cambiar a la bifurcación wireguard lib -- Solicite permiso de VPN en el primer inicio de VPN -- Versiones de golpe diff --git a/fastlane/metadata/android/es-ES/changelogs/33700.txt b/fastlane/metadata/android/es-ES/changelogs/33700.txt deleted file mode 100644 index 7da7939a..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/33700.txt +++ /dev/null @@ -1,2 +0,0 @@ -Novedades: -- Corrección de errores en la interfaz de usuario de Tunnel diff --git a/fastlane/metadata/android/es-ES/changelogs/33800.txt b/fastlane/metadata/android/es-ES/changelogs/33800.txt deleted file mode 100644 index df6d5e19..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/33800.txt +++ /dev/null @@ -1,4 +0,0 @@ -Novedades: -- Config editar corrección de errores de interfaz de usuario -- Añadir GrapheneOS primer lanzamiento AOVPN notificación -- Versiones Bump diff --git a/fastlane/metadata/android/es-ES/changelogs/33900.txt b/fastlane/metadata/android/es-ES/changelogs/33900.txt deleted file mode 100644 index 5fd33d15..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/33900.txt +++ /dev/null @@ -1,5 +0,0 @@ -Novedades: -- Añadir pantalla de registros -- Añadir bloqueo local de aplicaciones -- Añadir reiniciar vpn en ping fallido -- Varios errores corregidos diff --git a/fastlane/metadata/android/es-ES/changelogs/34000.txt b/fastlane/metadata/android/es-ES/changelogs/34000.txt deleted file mode 100644 index bb18a2d7..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/34000.txt +++ /dev/null @@ -1,5 +0,0 @@ -Novedades: -- Selección automática de túnel por nombre de WiFi -- Control automático de túneles a través de mosaicos y atajos. -- Reinicio automático del túnel manual después de un reinicio del sistema. -- Varias correcciones de errores y mejoras de rendimiento diff --git a/fastlane/metadata/android/es-ES/changelogs/34100.txt b/fastlane/metadata/android/es-ES/changelogs/34100.txt deleted file mode 100644 index 591011c1..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/34100.txt +++ /dev/null @@ -1,5 +0,0 @@ -Novedades: -- Mejora de la fiabilidad del túnel automático -- Mejora de la sincronización de azulejos -- Añadidos activos AndroidTV -- Añadida huella digital al apk diff --git a/fastlane/metadata/android/es-ES/changelogs/34200.txt b/fastlane/metadata/android/es-ES/changelogs/34200.txt deleted file mode 100644 index 93e5942f..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/34200.txt +++ /dev/null @@ -1,5 +0,0 @@ -Novedades: -- Corrección de la regresión del túnel de parada -- Añadir ofuscación de registros -- Añadir ocultar FAB en scroll -- Añadir localización turca diff --git a/fastlane/metadata/android/es-ES/changelogs/34202.txt b/fastlane/metadata/android/es-ES/changelogs/34202.txt deleted file mode 100644 index c23a57b1..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/34202.txt +++ /dev/null @@ -1,3 +0,0 @@ -Novedades: -- Agregue Amnezia junto con WireGuard -- Se corrigió el error de los accesos directos de la aplicación diff --git a/fastlane/metadata/android/es-ES/changelogs/34300.txt b/fastlane/metadata/android/es-ES/changelogs/34300.txt deleted file mode 100644 index 6aeb7beb..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/34300.txt +++ /dev/null @@ -1,6 +0,0 @@ -Novedades: -- Soporte oficial para AmneziaWG -- Importación/exportación de configuraciones de Amnezia -- El túnel automático se activará solo una vez por cambio de red -- Soporte para idiomas adicionales -- Otras correcciones de errores y mejoras diff --git a/fastlane/metadata/android/es-ES/changelogs/34400.txt b/fastlane/metadata/android/es-ES/changelogs/34400.txt deleted file mode 100644 index e412c6f3..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/34400.txt +++ /dev/null @@ -1,5 +0,0 @@ -Novedades: -- Mejorar la denominación de las importaciones de túneles -- Se solucionó el error del estado inicial de tunelización automática -- Manejo de errores mejorado -- Se solucionó el error de importación del zip de Amnezia diff --git a/fastlane/metadata/android/es-ES/changelogs/34500.txt b/fastlane/metadata/android/es-ES/changelogs/34500.txt deleted file mode 100644 index 6df6cb14..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/34500.txt +++ /dev/null @@ -1,5 +0,0 @@ -Novedades: -- Soporte de idiomas adicionales -- Corrección de errores en la tunelización automática de datos móviles -- Corrección del botón de acción flotante de AndroidTV -- Otras optimizaciones y mejoras diff --git a/fastlane/metadata/android/es-ES/changelogs/34600.txt b/fastlane/metadata/android/es-ES/changelogs/34600.txt deleted file mode 100644 index 9c76d1e8..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/34600.txt +++ /dev/null @@ -1,4 +0,0 @@ -Novedades: -- Se corrigieron problemas con la operación del túnel automático. -- Se solucionó el problema con la copia de seguridad de Android. -- Mayor número de versiones diff --git a/fastlane/metadata/android/es-ES/changelogs/34700.txt b/fastlane/metadata/android/es-ES/changelogs/34700.txt deleted file mode 100644 index 777f894d..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/34700.txt +++ /dev/null @@ -1,6 +0,0 @@ -Novedades: -- Solucionar problemas de bloqueo -- Mejorar el rendimiento de los mosaicos -- Volver a habilitar el bloqueo del PIN -- Hacer que reiniciar al arrancar sea una configuración -- Varias correcciones de errores y rendimiento diff --git a/fastlane/metadata/android/es-ES/changelogs/34800.txt b/fastlane/metadata/android/es-ES/changelogs/34800.txt deleted file mode 100644 index 716c7797..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/34800.txt +++ /dev/null @@ -1,5 +0,0 @@ -Novedades: -- Correcciones para el control del túnel de la interfaz de usuario de AndroidTV -- Corrige el error de bloqueo de retrato. -- Corrige el error de omisión del bloqueo del PIN -- Corrige el error de mosaico de túnel automático diff --git a/fastlane/metadata/android/es-ES/changelogs/35000.txt b/fastlane/metadata/android/es-ES/changelogs/35000.txt deleted file mode 100644 index 9d7c045f..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/35000.txt +++ /dev/null @@ -1,6 +0,0 @@ -Lo nuevo: -- Fiabilidad del túnel automático mejorada -- Compatible con tema claro/oscuro/dinámico -- Compatible con scripts pre/post up/down -- Notificación persistente del túnel quitada -- Correcciones y mejoras varias diff --git a/fastlane/metadata/android/es-ES/changelogs/35100.txt b/fastlane/metadata/android/es-ES/changelogs/35100.txt deleted file mode 100644 index 2aa629f4..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/35100.txt +++ /dev/null @@ -1,5 +0,0 @@ -Lo nuevo: -- Inicio de túneles en segundo plano corregido -- Podibilidad de reiniciar los servicios tras una actualización -- Mojorada la velocidad de las animaciones del UI -- Otras mejoras diff --git a/fastlane/metadata/android/es-ES/changelogs/35200.txt b/fastlane/metadata/android/es-ES/changelogs/35200.txt deleted file mode 100644 index cb9aeafa..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/35200.txt +++ /dev/null @@ -1,6 +0,0 @@ -Novedades: -- Añadido soporte de comodines para nombres wifi -- Edición en vivo de la configuración del túnel/auto-túnel mientras está activo -- Corrección de la lentitud en datos móviles -- Varias correcciones de errores y mejoras -- Optimización de la interfaz de usuario diff --git a/fastlane/metadata/android/es-ES/changelogs/35300.txt b/fastlane/metadata/android/es-ES/changelogs/35300.txt deleted file mode 100644 index cee82971..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/35300.txt +++ /dev/null @@ -1,6 +0,0 @@ -Novedades: -- Los dispositivos rooteados ahora obtienen el nombre wifi sin ubicación -- Mejoras en la pantalla de desplazamiento y compartición de registros -- Corrección de errores en la importación de túneles para AndroidTV 14 -- Mejoras en la interfaz de usuario de las estadísticas de túneles -- Otras correcciones de errores y mejoras diff --git a/fastlane/metadata/android/es-ES/changelogs/36000.txt b/fastlane/metadata/android/es-ES/changelogs/36000.txt deleted file mode 100644 index 486a3f7c..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/36000.txt +++ /dev/null @@ -1,6 +0,0 @@ -Novedades: -- Actualización de la interfaz de usuario -- Mejoras en la navegación de AndroidTV -- Corrección del error de descarga de la batería -- Se corrigieron los comodines con configuración opcional -- Otras mejoras diff --git a/fastlane/metadata/android/es-ES/changelogs/36100.txt b/fastlane/metadata/android/es-ES/changelogs/36100.txt deleted file mode 100644 index f2edb074..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/36100.txt +++ /dev/null @@ -1,7 +0,0 @@ -Novedades: -- Agregar túnel desde el portapapeles -- Añadir localizaciones -- Solucionar el error de descarga de la batería -- Corregir error de eliminación -- Mejorar la sincronización de los mosaicos del túnel -- Otras correcciones y mejoras diff --git a/fastlane/metadata/android/es-ES/changelogs/36200.txt b/fastlane/metadata/android/es-ES/changelogs/36200.txt deleted file mode 100644 index c9189624..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/36200.txt +++ /dev/null @@ -1,5 +0,0 @@ -Novedades: -- Interruptor de apagado VPN añadido con exclusión LAN -- Velocidad y fiabilidad mejoradas del túnel automático -- Sincronización de mosaicos mejorada -- Varias correcciones de errores y mejoras diff --git a/fastlane/metadata/android/es-ES/changelogs/36300.txt b/fastlane/metadata/android/es-ES/changelogs/36300.txt deleted file mode 100644 index cee8ef2f..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/36300.txt +++ /dev/null @@ -1,3 +0,0 @@ -Novedades: -- Error de conmutación del modo kernel corregido -- Se ha corregido un error de bloqueo de notificación diff --git a/fastlane/metadata/android/es-ES/changelogs/36500.txt b/fastlane/metadata/android/es-ES/changelogs/36500.txt deleted file mode 100644 index 001590c1..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/36500.txt +++ /dev/null @@ -1,6 +0,0 @@ -Novedades: -- Mejoras en la fiabilidad del túnel automático -- Corrección de guardado de túnel dividido y mejoras de rendimiento -- Corrección de errores en modo kernel -- Corrección de errores en mosaicos -- Otras correcciones y mejoras diff --git a/fastlane/metadata/android/es-ES/changelogs/36600.txt b/fastlane/metadata/android/es-ES/changelogs/36600.txt deleted file mode 100644 index f3649777..00000000 --- a/fastlane/metadata/android/es-ES/changelogs/36600.txt +++ /dev/null @@ -1,6 +0,0 @@ -Novedades: -- La función Ping ahora funciona independientemente del túnel automático -- Añadida acción para compatibilidad con Amnezia -- Añadida acción para excluir LAN del túnel -- Añadida la opción de ajuste del retardo de rebote para el túnel automático. -- Numerosas correcciones de errores y mejoras diff --git a/fastlane/metadata/android/fr-FR/changelogs/32100.txt b/fastlane/metadata/android/fr-FR/changelogs/32100.txt deleted file mode 100644 index d8f57bd2..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/32100.txt +++ /dev/null @@ -1,3 +0,0 @@ -Améliorations : -- Correction < problème de permission sur Android 9 -- Autres optimisations diff --git a/fastlane/metadata/android/fr-FR/changelogs/32200.txt b/fastlane/metadata/android/fr-FR/changelogs/32200.txt deleted file mode 100644 index 0cb61d40..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/32200.txt +++ /dev/null @@ -1,5 +0,0 @@ -Améliorations : -- Ajout des statistiques des tunnels sur l'écran principal -- Amélioration des paramètres de navigation AndroidTV -- Suppression des vibrations de notification -- Diverses autres corrections de bug diff --git a/fastlane/metadata/android/fr-FR/changelogs/32300.txt b/fastlane/metadata/android/fr-FR/changelogs/32300.txt deleted file mode 100644 index 4a23d7ce..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/32300.txt +++ /dev/null @@ -1,5 +0,0 @@ -Améliorations : -- Ajouter du support pour les données mobiles uniquement lors du tunnel automatique -- Amélioration de l'interface de support -- Mise à jour des liens de ressources -- Diverses autres corrections de bug diff --git a/fastlane/metadata/android/fr-FR/changelogs/32400.txt b/fastlane/metadata/android/fr-FR/changelogs/32400.txt deleted file mode 100644 index a98fde9e..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/32400.txt +++ /dev/null @@ -1,5 +0,0 @@ -Améliorations : -- Ajout de la prise en charge simple pour le noyau WireGuard -- Amélioration du flux de divulgation de l'emplacement -- Correction des permissions du tunnel automatique -- Diverses autres corrections de bugs sur l'interface utilisateur diff --git a/fastlane/metadata/android/fr-FR/changelogs/32500.txt b/fastlane/metadata/android/fr-FR/changelogs/32500.txt deleted file mode 100644 index 604d338b..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/32500.txt +++ /dev/null @@ -1,2 +0,0 @@ -Résolutions : -- Autorisations avant plan sur Android 14 diff --git a/fastlane/metadata/android/fr-FR/changelogs/33000.txt b/fastlane/metadata/android/fr-FR/changelogs/33000.txt deleted file mode 100644 index 25ce2400..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/33000.txt +++ /dev/null @@ -1,7 +0,0 @@ -Améliorations : -- Re-factorisation de la gestion d'état -- Amélioration de la navigation sur AndroidTV -- Optimisation de l'efficacité de l'auto-tunnel -- Navigation améliorée -- Fonction de pause pour l'auto-tunnel -- Nombreux bugs résolus diff --git a/fastlane/metadata/android/fr-FR/changelogs/33100.txt b/fastlane/metadata/android/fr-FR/changelogs/33100.txt deleted file mode 100644 index 25ce2400..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/33100.txt +++ /dev/null @@ -1,7 +0,0 @@ -Améliorations : -- Re-factorisation de la gestion d'état -- Amélioration de la navigation sur AndroidTV -- Optimisation de l'efficacité de l'auto-tunnel -- Navigation améliorée -- Fonction de pause pour l'auto-tunnel -- Nombreux bugs résolus diff --git a/fastlane/metadata/android/fr-FR/changelogs/33200.txt b/fastlane/metadata/android/fr-FR/changelogs/33200.txt deleted file mode 100644 index d96460b7..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/33200.txt +++ /dev/null @@ -1,8 +0,0 @@ -Améliorations : -- Re-factorisation de la gestion d'état -- Amélioration de la navigation sur AndroidTV -- Optimisation de l'efficacité de l'auto-tunnel -- Navigation améliorée -- Fonction de pause pour l'auto-tunnel -- Correction du démarrage de l'auto-tunnel en premier plan -- Nombreux bugs résolus diff --git a/fastlane/metadata/android/fr-FR/changelogs/33300.txt b/fastlane/metadata/android/fr-FR/changelogs/33300.txt deleted file mode 100644 index daa1d39f..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/33300.txt +++ /dev/null @@ -1,7 +0,0 @@ -Améliorations : -- Ajout d'une confirmation lors de la suppression d'un tunnel -- Ajout de la permission d'usage de la batterie en arrière plan -Corrections  : -- Bug de gel lors de la désactivation du tunnel -- Bug du champ « À » dans l'email -- Édition de la configuration avec champ DNS vide diff --git a/fastlane/metadata/android/fr-FR/changelogs/33400.txt b/fastlane/metadata/android/fr-FR/changelogs/33400.txt deleted file mode 100644 index eee7b3be..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/33400.txt +++ /dev/null @@ -1,3 +0,0 @@ -Améliorations : -- Correction du problème de non-enregistrement lors de la création de configuration -- Mise à jour des versions diff --git a/fastlane/metadata/android/fr-FR/changelogs/33401.txt b/fastlane/metadata/android/fr-FR/changelogs/33401.txt deleted file mode 100644 index d7e77ee4..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/33401.txt +++ /dev/null @@ -1,2 +0,0 @@ -Quoi de neuf : -- Il s'agit d'une version de test pour l'intégration continue (CI) diff --git a/fastlane/metadata/android/fr-FR/changelogs/33500.txt b/fastlane/metadata/android/fr-FR/changelogs/33500.txt deleted file mode 100644 index 2f7e3037..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/33500.txt +++ /dev/null @@ -1,5 +0,0 @@ -Quoi de neuf : -- Démarrage automatique au redémarrage pour le mode noyau VPN permanent -- Prise en charge des icônes de thème adaptatif -- Correction des icônes de notification et des icônes de réglages rapide -- Correction des icônes AndroidTV diff --git a/fastlane/metadata/android/fr-FR/changelogs/33600.txt b/fastlane/metadata/android/fr-FR/changelogs/33600.txt deleted file mode 100644 index f9ccaf82..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/33600.txt +++ /dev/null @@ -1,5 +0,0 @@ -Quoi de neuf : -- Amélioration du flux de première ouverture -- Passage à un fork de la bibliothèque WireGuard -- Demande d'autorisation VPN lors du premier démarrage du VPN -- Mise à jour des versions diff --git a/fastlane/metadata/android/fr-FR/changelogs/33700.txt b/fastlane/metadata/android/fr-FR/changelogs/33700.txt deleted file mode 100644 index 9f4c6144..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/33700.txt +++ /dev/null @@ -1,2 +0,0 @@ -Quoi de neuf : -- Résolution d'un bug d'affichage du tunnel sur l'interface utilisateur diff --git a/fastlane/metadata/android/fr-FR/changelogs/33800.txt b/fastlane/metadata/android/fr-FR/changelogs/33800.txt deleted file mode 100644 index 7a6ff1bb..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/33800.txt +++ /dev/null @@ -1,4 +0,0 @@ -Quoi de neuf : -- Correction du bug d'affichage de l'interface utilisateur lors de l'édition de la configuration -- Ajout d'une notification VPN permanent au premier lancement pour GrapheneOS -- Mise à jour des versions diff --git a/fastlane/metadata/android/fr-FR/changelogs/33900.txt b/fastlane/metadata/android/fr-FR/changelogs/33900.txt deleted file mode 100644 index 13b3b606..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/33900.txt +++ /dev/null @@ -1,5 +0,0 @@ -Quoi de neuf : -- Ajout d'un écran de logs -- Ajout d'un verrouillage local de l'application -- Ajout de la fonctionnalité de redémarrage du VPN en cas d'échec du ping -- Diverses corrections de bug diff --git a/fastlane/metadata/android/fr-FR/changelogs/34000.txt b/fastlane/metadata/android/fr-FR/changelogs/34000.txt deleted file mode 100644 index 1ab27a35..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/34000.txt +++ /dev/null @@ -1,5 +0,0 @@ -Quoi de neuf : -- Tunnel automatique vers des tunnels spécifiques selon le nom du réseau Wi-Fi -- Contrôle du tunnel automatique depuis les raccourcis de la barre de réglages rapides et les raccourcis -- Démarrage automatique du tunnel manuel s'il était actif avant le redémarrage -- Divers correctifs de bugs et amélioration des performances diff --git a/fastlane/metadata/android/fr-FR/changelogs/34100.txt b/fastlane/metadata/android/fr-FR/changelogs/34100.txt deleted file mode 100644 index aee06e28..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/34100.txt +++ /dev/null @@ -1,5 +0,0 @@ -Quoi de neuf : -- Amélioration de la fiabilité des tunnels automatique -- Synchronisation améliorée avec la barre de réglages rapides -- Ajout de ressources pour AndroidTV -- Ajout de l'empreinte apk diff --git a/fastlane/metadata/android/fr-FR/changelogs/34200.txt b/fastlane/metadata/android/fr-FR/changelogs/34200.txt deleted file mode 100644 index 067cd3f2..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/34200.txt +++ /dev/null @@ -1,5 +0,0 @@ -Quoi de neuf : -- Correction de la régression lors de l'arrêt du tunnel -- Ajout de l’offuscation des logs -- Ajout de l'option de masquage du bouton d'action flottant lors du défilement -- Ajout de la localisation turque diff --git a/fastlane/metadata/android/fr-FR/changelogs/34202.txt b/fastlane/metadata/android/fr-FR/changelogs/34202.txt deleted file mode 100644 index c0540809..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/34202.txt +++ /dev/null @@ -1,3 +0,0 @@ -Quoi de neuf : -- Ajout d'Amnezia en parallèle avec WireGuard -- Correction du bug sur les raccourcis d'application diff --git a/fastlane/metadata/android/fr-FR/changelogs/34300.txt b/fastlane/metadata/android/fr-FR/changelogs/34300.txt deleted file mode 100644 index e522f40f..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/34300.txt +++ /dev/null @@ -1,6 +0,0 @@ -Quoi de neuf : -- Prise en charge officielle d'AmneziaWG -- Import/export de configurations Amnezia -- Tunnel automatique activé une seule fois par changement de réseau -- Prise en charge de langues supplémentaires -- Autres corrections de bugs et améliorations diff --git a/fastlane/metadata/android/fr-FR/changelogs/34400.txt b/fastlane/metadata/android/fr-FR/changelogs/34400.txt deleted file mode 100644 index a1143fb0..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/34400.txt +++ /dev/null @@ -1,5 +0,0 @@ -Quoi de neuf : -- Amélioration du nommage lors de l'importation du tunnel -- Correction du bug d'état d'initialisation de l'auto-tunnel -- Amélioration de la gestion des erreurs -- Correction du bug d'importation de fichiers zip Amnezia diff --git a/fastlane/metadata/android/fr-FR/changelogs/34500.txt b/fastlane/metadata/android/fr-FR/changelogs/34500.txt deleted file mode 100644 index a1442c09..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/34500.txt +++ /dev/null @@ -1,5 +0,0 @@ -Quoi de neuf : -- Prise en charge de langues supplémentaires -- Correction du bug de l'auto-tunnel avec les données mobiles -- Correction du bouton d'action flottant sur AndroidTV -- Autres optimisations et améliorations diff --git a/fastlane/metadata/android/fr-FR/changelogs/34600.txt b/fastlane/metadata/android/fr-FR/changelogs/34600.txt deleted file mode 100644 index b3e4eb42..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/34600.txt +++ /dev/null @@ -1,4 +0,0 @@ -Quoi de neuf : -- Correction des bugs de l'auto-tunnel -- Correction du bug de sauvegarde Android -- Mise à jour des versions diff --git a/fastlane/metadata/android/fr-FR/changelogs/34700.txt b/fastlane/metadata/android/fr-FR/changelogs/34700.txt deleted file mode 100644 index 2dc5c5b2..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/34700.txt +++ /dev/null @@ -1,6 +0,0 @@ -Quoi de neuf : -- Correction des problèmes de crash -- Amélioration des performances de la barre de réglages rapides -- Réactivation du verrouillage par code PIN -- La fonctionnalité du redémarrage au démarrage devient une option -- Diverses améliorations des performances et corrections de bugs diff --git a/fastlane/metadata/android/fr-FR/changelogs/34800.txt b/fastlane/metadata/android/fr-FR/changelogs/34800.txt deleted file mode 100644 index 7cdf02ce..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/34800.txt +++ /dev/null @@ -1,5 +0,0 @@ -Quoi de neuf : -- Corrections de l'interface utilisateur de contrôle du tunnel sur AndroidTV -- Correction du bug de verrouillage en mode portrait -- Correction du bug de contournement du verrouillage PIN -- Correction du bug de la barre de réglages rapides lors de l'auto-tunnel diff --git a/fastlane/metadata/android/fr-FR/changelogs/35000.txt b/fastlane/metadata/android/fr-FR/changelogs/35000.txt deleted file mode 100644 index 2ffe6a8f..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/35000.txt +++ /dev/null @@ -1,6 +0,0 @@ -Quoi de neuf : -- Amélioration de la fiabilité des tunnels automatiques -- Prise en charge des thèmes clair/sombre/dynamique -- Prise en charge des scripts pré/post up/down -- Supprimer la notification persistante du tunnel -- Diverses autres corrections et améliorations diff --git a/fastlane/metadata/android/fr-FR/changelogs/35100.txt b/fastlane/metadata/android/fr-FR/changelogs/35100.txt deleted file mode 100644 index 0f59780f..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/35100.txt +++ /dev/null @@ -1,5 +0,0 @@ -Quoi de neuf : -- Corrections des problèmes de lancement des tunnels en arrière-plan -- Prise en charge du redémarrage des services après une mise à jour -- Améliorations de la vitesse des animations de l'interface utilisateur -- Autres optimisations diff --git a/fastlane/metadata/android/fr-FR/changelogs/35200.txt b/fastlane/metadata/android/fr-FR/changelogs/35200.txt deleted file mode 100644 index 902af624..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/35200.txt +++ /dev/null @@ -1,6 +0,0 @@ -Quoi de neuf : -- Ajout du support des « wildcard » pour les noms wifi -- Modification des réglages des tunnel/auto-tunnel pendant leur fonctionnement -- Correction de la lenteur sur les données mobiles -- Divers correctifs de bogues et améliorations -- Optimisations de l'interface utilisateur diff --git a/fastlane/metadata/android/fr-FR/changelogs/35300.txt b/fastlane/metadata/android/fr-FR/changelogs/35300.txt deleted file mode 100644 index 5be3c5ba..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/35300.txt +++ /dev/null @@ -1,6 +0,0 @@ -Quoi de neuf : -- Les appareils rootés récupèrent désormais le nom du Wi-Fi sans la localisation -- Améliorations du défilement et du partage de l'écran des journaux -- Corrections de bugs d'importation de tunnel pour AndroidTV 14 -- Améliorations de l'interface utilisateur des statistiques de tunnel -- Autres corrections de bugs et améliorations diff --git a/fastlane/metadata/android/fr-FR/changelogs/36000.txt b/fastlane/metadata/android/fr-FR/changelogs/36000.txt deleted file mode 100644 index 79eb2a64..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/36000.txt +++ /dev/null @@ -1,6 +0,0 @@ -Quoi de neuf : -- Mise à jour de l'interface utilisateur -- Améliorations de la navigation sur Android TV -- Correction d'un bug de surconsommation de la batterie -- Correction des wildcards avec réglage optionnel -- Autres améliorations diff --git a/fastlane/metadata/android/fr-FR/changelogs/36100.txt b/fastlane/metadata/android/fr-FR/changelogs/36100.txt deleted file mode 100644 index 1e549a12..00000000 --- a/fastlane/metadata/android/fr-FR/changelogs/36100.txt +++ /dev/null @@ -1,7 +0,0 @@ -Quoi de neuf : -- Ajout des tunnels depuis le presse-papiers -- Ajout des localisations -- Correction d'un bug de surconsommation de la batterie -- Correction d'un bug de suppression -- Amélioration de la synchronisation des tuiles du tunnel. -- Autres améliorations diff --git a/fastlane/metadata/android/id/changelogs/32100.txt b/fastlane/metadata/android/id/changelogs/32100.txt deleted file mode 100644 index fd08e6e5..00000000 --- a/fastlane/metadata/android/id/changelogs/32100.txt +++ /dev/null @@ -1,3 +0,0 @@ -Peningkatan: -- Perbaikan < Android 9 bug perizinian -- Optimisasi lainnya diff --git a/fastlane/metadata/android/id/changelogs/32200.txt b/fastlane/metadata/android/id/changelogs/32200.txt deleted file mode 100644 index ce3405e5..00000000 --- a/fastlane/metadata/android/id/changelogs/32200.txt +++ /dev/null @@ -1,5 +0,0 @@ -Peningkatan: -- Tambahkan statistik terowongan ke layar utama -- Tingkatkan layar pengaturan navigasi AndroidTV -- Hapus getaran notifikasi -- Berbagai perbaikan bug lainnya diff --git a/fastlane/metadata/android/id/changelogs/32300.txt b/fastlane/metadata/android/id/changelogs/32300.txt deleted file mode 100644 index 8a92bcbb..00000000 --- a/fastlane/metadata/android/id/changelogs/32300.txt +++ /dev/null @@ -1,5 +0,0 @@ -Peningkatan: -- Menambahkan dukungan untuk tunnel otomatis hanya data seluler -- Meningkatkan UI layar dukungan -- Perbarui tautan sumber daya -- Berbagai perbaikan bug lainnya diff --git a/fastlane/metadata/android/id/changelogs/32400.txt b/fastlane/metadata/android/id/changelogs/32400.txt deleted file mode 100644 index e5005483..00000000 --- a/fastlane/metadata/android/id/changelogs/32400.txt +++ /dev/null @@ -1,5 +0,0 @@ -Peningkatan: -- Tambahkan dukungan dasar WireGuard Kernel -- Alur pengungkapan lokasi yang ditingkatkan -- Perbaiki bug izin tunnel otomatis -- Berbagai perbaikan bug UI lainnya diff --git a/fastlane/metadata/android/id/changelogs/32500.txt b/fastlane/metadata/android/id/changelogs/32500.txt deleted file mode 100644 index 4b4917cc..00000000 --- a/fastlane/metadata/android/id/changelogs/32500.txt +++ /dev/null @@ -1,2 +0,0 @@ -Perbaikan: -- Izin latar depan Android 14 diff --git a/fastlane/metadata/android/id/changelogs/33000.txt b/fastlane/metadata/android/id/changelogs/33000.txt deleted file mode 100644 index 83d7ff05..00000000 --- a/fastlane/metadata/android/id/changelogs/33000.txt +++ /dev/null @@ -1,7 +0,0 @@ -Peningkatan: -- Refaktor manajemen status -- Tingkatkan navigasi AndroidTV -- Meningkatkan efisiensi tunnel otomatis -- Meningkatkan navigasi -- Fitur jeda tunnel otomatis -- Banyak perbaikan bug diff --git a/fastlane/metadata/android/id/changelogs/33100.txt b/fastlane/metadata/android/id/changelogs/33100.txt deleted file mode 100644 index 83d7ff05..00000000 --- a/fastlane/metadata/android/id/changelogs/33100.txt +++ /dev/null @@ -1,7 +0,0 @@ -Peningkatan: -- Refaktor manajemen status -- Tingkatkan navigasi AndroidTV -- Meningkatkan efisiensi tunnel otomatis -- Meningkatkan navigasi -- Fitur jeda tunnel otomatis -- Banyak perbaikan bug diff --git a/fastlane/metadata/android/id/changelogs/33200.txt b/fastlane/metadata/android/id/changelogs/33200.txt deleted file mode 100644 index c4251619..00000000 --- a/fastlane/metadata/android/id/changelogs/33200.txt +++ /dev/null @@ -1,8 +0,0 @@ -Peningkatan: -- Refaktor manajemen status -- Tingkatkan navigasi AndroidTV -- Meningkatkan efisiensi tunnel otomatis -- Meningkatkan navigasi -- Fitur jeda tunnel otomatis -- Perbaiki tunnel otomatis mulai latar depan -- Banyak perbaikan bug diff --git a/fastlane/metadata/android/id/changelogs/33300.txt b/fastlane/metadata/android/id/changelogs/33300.txt deleted file mode 100644 index fd28260a..00000000 --- a/fastlane/metadata/android/id/changelogs/33300.txt +++ /dev/null @@ -1,7 +0,0 @@ -Peningkatan: -- Menambahkan konfirmasi hapus tunnel -- Menambahkan izin latar belakang baterai -Perbaikan: -- Tunnel menonaktifkan bug beku -- Email ke bug lapangan -- Konfigurasi edit DNS kosong diff --git a/fastlane/metadata/android/id/changelogs/33400.txt b/fastlane/metadata/android/id/changelogs/33400.txt deleted file mode 100644 index c1ad86d7..00000000 --- a/fastlane/metadata/android/id/changelogs/33400.txt +++ /dev/null @@ -1,3 +0,0 @@ -Peningkatan: -- Memperbaiki konfigurasi buat yang tidak disimpan -- Versi yang dibenturkan diff --git a/fastlane/metadata/android/id/changelogs/33401.txt b/fastlane/metadata/android/id/changelogs/33401.txt deleted file mode 100644 index 1855623a..00000000 --- a/fastlane/metadata/android/id/changelogs/33401.txt +++ /dev/null @@ -1,2 +0,0 @@ -Apa yang baru: -- Ini adalah versi pengujian CI diff --git a/fastlane/metadata/android/id/changelogs/33500.txt b/fastlane/metadata/android/id/changelogs/33500.txt deleted file mode 100644 index e3b85d27..00000000 --- a/fastlane/metadata/android/id/changelogs/33500.txt +++ /dev/null @@ -1,5 +0,0 @@ -Apa yang baru: -- Mulai otomatis saat reboot untuk mode kernel VPN Always-On -- Dukungan untuk ikon tema adaptif -- Perbaiki ikon notifikasi, ikon kartu -- Perbaiki ikon AndroidTV diff --git a/fastlane/metadata/android/id/changelogs/33600.txt b/fastlane/metadata/android/id/changelogs/33600.txt deleted file mode 100644 index d8b9af51..00000000 --- a/fastlane/metadata/android/id/changelogs/33600.txt +++ /dev/null @@ -1,5 +0,0 @@ -Apa yang baru: -- Meningkatkan alur peluncuran pertama -- Beralih ke garpu lib wireguard -- Minta izin VPN pada awal VPN pertama -- Versi bump diff --git a/fastlane/metadata/android/id/changelogs/33700.txt b/fastlane/metadata/android/id/changelogs/33700.txt deleted file mode 100644 index c80a48a2..00000000 --- a/fastlane/metadata/android/id/changelogs/33700.txt +++ /dev/null @@ -1,2 +0,0 @@ -Apa yang baru: -- Perbaikan bug UI tampilan tunnel diff --git a/fastlane/metadata/android/id/changelogs/33800.txt b/fastlane/metadata/android/id/changelogs/33800.txt deleted file mode 100644 index 225adad8..00000000 --- a/fastlane/metadata/android/id/changelogs/33800.txt +++ /dev/null @@ -1,4 +0,0 @@ -Apa yang baru: -- Perbaikan bug UI edit konfigurasi -- Tambahkan pemberitahuan AOVPN peluncuran pertama GrapheneOS -- Versi bump diff --git a/fastlane/metadata/android/id/changelogs/33900.txt b/fastlane/metadata/android/id/changelogs/33900.txt deleted file mode 100644 index d5c5f9dc..00000000 --- a/fastlane/metadata/android/id/changelogs/33900.txt +++ /dev/null @@ -1,5 +0,0 @@ -Apa yang baru: -- Layar Tambahkan log -- Menambahkan kunci aplikasi lokal -- Tambahkan restart vpn pada ping yang gagal -- Berbagai perbaikan bug diff --git a/fastlane/metadata/android/id/changelogs/34000.txt b/fastlane/metadata/android/id/changelogs/34000.txt deleted file mode 100644 index ebc2a558..00000000 --- a/fastlane/metadata/android/id/changelogs/34000.txt +++ /dev/null @@ -1,5 +0,0 @@ -Apa yang baru: -- Tunnel otomatis ke tunnel tertentu dengan nama wifi -- Kontrol tunnel otomatis dari ubin dan pintasan -- Mulai otomatis tunnel manual jika aktif sebelum reboot -- Berbagai perbaikan bug dan peningkatan kinerja diff --git a/fastlane/metadata/android/id/changelogs/34100.txt b/fastlane/metadata/android/id/changelogs/34100.txt deleted file mode 100644 index d7ea8722..00000000 --- a/fastlane/metadata/android/id/changelogs/34100.txt +++ /dev/null @@ -1,5 +0,0 @@ -Apa yang baru: -- Peningkatan keandalan tunnel otomatis -- Sinkronisasi kartu yang ditingkatkan -- Menambahkan aset AndroidTV -- Menambahkan sidik jari apk diff --git a/fastlane/metadata/android/id/changelogs/34200.txt b/fastlane/metadata/android/id/changelogs/34200.txt deleted file mode 100644 index 237fc011..00000000 --- a/fastlane/metadata/android/id/changelogs/34200.txt +++ /dev/null @@ -1,5 +0,0 @@ -Apa yang baru: -- Perbaiki regresi stop tunnel -- Tambahkan pengaburan log -- Tambahkan sembunyikan FAB pada gulir -- Tambahkan pelokalan Turki diff --git a/fastlane/metadata/android/id/changelogs/34202.txt b/fastlane/metadata/android/id/changelogs/34202.txt deleted file mode 100644 index 72021477..00000000 --- a/fastlane/metadata/android/id/changelogs/34202.txt +++ /dev/null @@ -1,3 +0,0 @@ -Apa yang baru: -- Tambahkan Amnezia secara berdampingan dengan WireGuard -- Perbaiki bug pintasan aplikasi diff --git a/fastlane/metadata/android/id/changelogs/34300.txt b/fastlane/metadata/android/id/changelogs/34300.txt deleted file mode 100644 index f79fc93b..00000000 --- a/fastlane/metadata/android/id/changelogs/34300.txt +++ /dev/null @@ -1,6 +0,0 @@ -Apa yang baru: -- Dukungan resmi untuk AmneziaWG -- Impor/ekspor untuk konfigurasi Amnezia -- Terowongan otomatis untuk hanya beralih sekali per perubahan jaringan -- Dukungan bahasa tambahan -- Perbaikan dan peningkatan bug lainnya diff --git a/fastlane/metadata/android/id/changelogs/34400.txt b/fastlane/metadata/android/id/changelogs/34400.txt deleted file mode 100644 index d5925d9a..00000000 --- a/fastlane/metadata/android/id/changelogs/34400.txt +++ /dev/null @@ -1,5 +0,0 @@ -Apa yang baru: -- Meningkatkan penamaan impor tunnel -- Perbaiki bug status init tunnel otomatis -- Penanganan kesalahan yang ditingkatkan -- Perbaiki bug impor zip Amnezia diff --git a/fastlane/metadata/android/id/changelogs/34500.txt b/fastlane/metadata/android/id/changelogs/34500.txt deleted file mode 100644 index 976415ba..00000000 --- a/fastlane/metadata/android/id/changelogs/34500.txt +++ /dev/null @@ -1,5 +0,0 @@ -Apa yang baru: -- Dukungan bahasa tambahan -- Perbaikan bug data seluler tunnel otomatis -- Perbaikan tombol tindakan mengambang AndroidTV -- Pengoptimalan dan penyempurnaan lainnya diff --git a/fastlane/metadata/android/id/changelogs/34600.txt b/fastlane/metadata/android/id/changelogs/34600.txt deleted file mode 100644 index b7cd9d63..00000000 --- a/fastlane/metadata/android/id/changelogs/34600.txt +++ /dev/null @@ -1,4 +0,0 @@ -Apa yang baru: -- Memperbaiki bug tunnel otomatis -- Memperbaiki bug pencadangan android -- Versi bump diff --git a/fastlane/metadata/android/id/changelogs/34700.txt b/fastlane/metadata/android/id/changelogs/34700.txt deleted file mode 100644 index e21165b9..00000000 --- a/fastlane/metadata/android/id/changelogs/34700.txt +++ /dev/null @@ -1,6 +0,0 @@ -Apa yang baru: -- Perbaiki masalah error -- Meningkatkan kinerja kartu -- Aktifkan kembali kunci pin -- Jadikan restart saat boot sebagai pengaturan -- Berbagai perbaikan kinerja dan bug diff --git a/fastlane/metadata/android/id/changelogs/34800.txt b/fastlane/metadata/android/id/changelogs/34800.txt deleted file mode 100644 index 0038b121..00000000 --- a/fastlane/metadata/android/id/changelogs/34800.txt +++ /dev/null @@ -1,5 +0,0 @@ -Apa yang baru: -- Perbaikan untuk kontrol tunnel UI AndroidTV -- Perbaikan bug kunci potret -- Perbaikan bug bypass kunci pin -- Perbaikan bug ubin tunnel otomatis diff --git a/fastlane/metadata/android/id/changelogs/35000.txt b/fastlane/metadata/android/id/changelogs/35000.txt deleted file mode 100644 index 44b4cd41..00000000 --- a/fastlane/metadata/android/id/changelogs/35000.txt +++ /dev/null @@ -1,6 +0,0 @@ -Apa yang baru: -- Meningkatkan keandalan tunnel otomatis -- Tambahkan dukungan untuk tema terang/gelap/dinamis -- Tambahkan dukungan untuk skrip pra/pasca naik/turun -- Hapus pemberitahuan persisten tunnel -- Berbagai perbaikan dan peningkatan lainnya diff --git a/fastlane/metadata/android/id/changelogs/35100.txt b/fastlane/metadata/android/id/changelogs/35100.txt deleted file mode 100644 index 5e344422..00000000 --- a/fastlane/metadata/android/id/changelogs/35100.txt +++ /dev/null @@ -1,5 +0,0 @@ -Apa yang baru: -- Perbaikan untuk tunnel yang tidak diluncurkan dari latar belakang -- Menambahkan dukungan untuk layanan restart setelah pembaruan -- Peningkatan kecepatan animasi UI -- Pengoptimalan lainnya diff --git a/fastlane/metadata/android/id/changelogs/35200.txt b/fastlane/metadata/android/id/changelogs/35200.txt deleted file mode 100644 index 700c489a..00000000 --- a/fastlane/metadata/android/id/changelogs/35200.txt +++ /dev/null @@ -1,6 +0,0 @@ -Apa yang baru: -- Menambahkan dukungan wildcard untuk nama wifi -- Pengeditan langsung pengaturan tunnel/tunnel otomatis saat aktif -- Memperbaiki kelambatan data seluler -- Berbagai perbaikan dan peningkatan bug -- Pengoptimalan UI diff --git a/fastlane/metadata/android/id/changelogs/35300.txt b/fastlane/metadata/android/id/changelogs/35300.txt deleted file mode 100644 index 1eb0ac7e..00000000 --- a/fastlane/metadata/android/id/changelogs/35300.txt +++ /dev/null @@ -1,6 +0,0 @@ -Apa yang baru: -- Perangkat yang di-rooting sekarang mendapatkan nama wifi tanpa lokasi -- Memperbaiki scroll layar dan berbagi -- Perbaikan bug impor tunnel untuk AndroidTV 14 -- Penyempurnaan UI statistik tunnel -- Perbaikan dan peningkatan bug lainnya diff --git a/fastlane/metadata/android/it-IT/changelogs/32100.txt b/fastlane/metadata/android/it-IT/changelogs/32100.txt deleted file mode 100644 index 46ab9c40..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/32100.txt +++ /dev/null @@ -1,3 +0,0 @@ -Migliorie: -- Correzione bug permessi in < Android 9 -- Altre ottimizzazioni diff --git a/fastlane/metadata/android/it-IT/changelogs/32200.txt b/fastlane/metadata/android/it-IT/changelogs/32200.txt deleted file mode 100644 index e871130b..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/32200.txt +++ /dev/null @@ -1,5 +0,0 @@ -Migliorie: -- Aggiunge statistiche tunnel alla schermata principale -- Migliorata schermata impostazioni per navigazione AndroidTV -- Rimossa vibrazione notifica -- Varie altre correzioni di bug diff --git a/fastlane/metadata/android/it-IT/changelogs/32300.txt b/fastlane/metadata/android/it-IT/changelogs/32300.txt deleted file mode 100644 index e82522c4..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/32300.txt +++ /dev/null @@ -1,5 +0,0 @@ -Migliorie: -- Aggiunge supporto a tunnel automatico su soli dati mobili -- Migliorata UI schermata supporto -- Aggiornati link risorse -- Varie altre correzioni di bug diff --git a/fastlane/metadata/android/it-IT/changelogs/32400.txt b/fastlane/metadata/android/it-IT/changelogs/32400.txt deleted file mode 100644 index 4ed17d03..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/32400.txt +++ /dev/null @@ -1,5 +0,0 @@ -Migliorie: -- Aggiunge supporto di base a WireGuard Kernel -- Flusso di divulgazione della posizione migliorato -- Corretto bug permessi auto-tunnel -- Varie altre correzioni di bug UI diff --git a/fastlane/metadata/android/it-IT/changelogs/32500.txt b/fastlane/metadata/android/it-IT/changelogs/32500.txt deleted file mode 100644 index da9964e4..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/32500.txt +++ /dev/null @@ -1,2 +0,0 @@ -Correzioni: -- Permessi primo piano Android 14 diff --git a/fastlane/metadata/android/it-IT/changelogs/33000.txt b/fastlane/metadata/android/it-IT/changelogs/33000.txt deleted file mode 100644 index d8fdcb0f..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/33000.txt +++ /dev/null @@ -1,7 +0,0 @@ -Migliorie: -- Rivista gestione stato -- Migliorata navigazione AndroidTV -- Migliorata efficienza auto-tunneling -- Migliorata navigazione -- Funzione pausa auto-tunneling -- Tanti problemi risolti diff --git a/fastlane/metadata/android/it-IT/changelogs/33100.txt b/fastlane/metadata/android/it-IT/changelogs/33100.txt deleted file mode 100644 index 877d752a..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/33100.txt +++ /dev/null @@ -1,7 +0,0 @@ -Migliorie: -- Rivista gestione stati -- Migliorata navigazione AndroidTV -- Migliorata efficienza auto-tunneling -- Migliorata navigazione -- Nuova funzione pausa auto-tunneling -- Molte correzioni di bug diff --git a/fastlane/metadata/android/it-IT/changelogs/33200.txt b/fastlane/metadata/android/it-IT/changelogs/33200.txt deleted file mode 100644 index 9a8ad031..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/33200.txt +++ /dev/null @@ -1,8 +0,0 @@ -Migliorie: -- Rivista gestione stati -- Migliorata navigazione AndroidTV -- Migliorata efficienza auto-tunneling -- Migliorata navigazione -- Nuova funzione pausa auto-tunneling -- Correzione avvio auto-tunnel in primo piano -- Molte correzioni di bug diff --git a/fastlane/metadata/android/it-IT/changelogs/33300.txt b/fastlane/metadata/android/it-IT/changelogs/33300.txt deleted file mode 100644 index c7ce8bad..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/33300.txt +++ /dev/null @@ -1,7 +0,0 @@ -Migliorie: -- Aggiunta conferma eliminazione tunnel -- Aggiunto permesso batteria in background -Correzioni: -- Bug disabilita tunnel bloccato -- Bug campo email a -- Modifica configurazione DNS vuoto diff --git a/fastlane/metadata/android/it-IT/changelogs/33400.txt b/fastlane/metadata/android/it-IT/changelogs/33400.txt deleted file mode 100644 index 72febefe..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/33400.txt +++ /dev/null @@ -1,3 +0,0 @@ -Migliorie: -- Corretto mancato salvataggio creazione configurazione -- Aggiornate versioni diff --git a/fastlane/metadata/android/it-IT/changelogs/33401.txt b/fastlane/metadata/android/it-IT/changelogs/33401.txt deleted file mode 100644 index e4127062..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/33401.txt +++ /dev/null @@ -1,2 +0,0 @@ -Novità: -- Questa è una versione di test CI diff --git a/fastlane/metadata/android/it-IT/changelogs/33500.txt b/fastlane/metadata/android/it-IT/changelogs/33500.txt deleted file mode 100644 index 4078faad..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/33500.txt +++ /dev/null @@ -1,5 +0,0 @@ -Novità: -- Auto-avvio al riavvio per modalità kernel VPN sempre attiva -- Supporto ad icone per tema adattive -- Corrette icone notifica, icone tile -- Corrette icone AndroidTV diff --git a/fastlane/metadata/android/it-IT/changelogs/33600.txt b/fastlane/metadata/android/it-IT/changelogs/33600.txt deleted file mode 100644 index 8d820849..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/33600.txt +++ /dev/null @@ -1,5 +0,0 @@ -Novità: -- Migliorato flusso avvio -- Passaggio a fork della libreria wireguard -- Richiesta permesso VPN al primo avvio VPN -- Aggiornate versioni diff --git a/fastlane/metadata/android/it-IT/changelogs/33700.txt b/fastlane/metadata/android/it-IT/changelogs/33700.txt deleted file mode 100644 index c030f2d9..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/33700.txt +++ /dev/null @@ -1,2 +0,0 @@ -Novità: -- Risolti problemi UI visualizzazione tunnel diff --git a/fastlane/metadata/android/it-IT/changelogs/33800.txt b/fastlane/metadata/android/it-IT/changelogs/33800.txt deleted file mode 100644 index 7e5cc2af..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/33800.txt +++ /dev/null @@ -1,4 +0,0 @@ -Novità: -- Risolto problema modifica configurazione -- Aggiunta notifica GrapheneOS al primo lancio AOVPN -- Aggiornate versioni diff --git a/fastlane/metadata/android/it-IT/changelogs/33900.txt b/fastlane/metadata/android/it-IT/changelogs/33900.txt deleted file mode 100644 index 752fe1ec..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/33900.txt +++ /dev/null @@ -1,5 +0,0 @@ -Novità: -- Aggiunte schermate log -- Aggiunto blocco app locale -- Aggiunto riavvia vpn quando fallisce il ping -- Varie risoluzioni di problemi diff --git a/fastlane/metadata/android/it-IT/changelogs/34000.txt b/fastlane/metadata/android/it-IT/changelogs/34000.txt deleted file mode 100644 index d5af823b..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/34000.txt +++ /dev/null @@ -1,5 +0,0 @@ -Novità: -- Auto tunnel a specifici tunnel su nome wifi -- Controllo auto tunnel da tile e scorciatoia -- Avvio automatico tunnel manuale se era attivo prima del riavvio -- Varie risoluzioni di problemi e migliorie di prestazioni diff --git a/fastlane/metadata/android/it-IT/changelogs/34100.txt b/fastlane/metadata/android/it-IT/changelogs/34100.txt deleted file mode 100644 index 874dcc2f..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/34100.txt +++ /dev/null @@ -1,5 +0,0 @@ -Novità: -- Migliorata stabilità auto tunnel -- Migliorata sincronizzazione tile -- Aggiunte risorse AndroidTV -- Aggiunta firma apk diff --git a/fastlane/metadata/android/it-IT/changelogs/34200.txt b/fastlane/metadata/android/it-IT/changelogs/34200.txt deleted file mode 100644 index 81b69641..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/34200.txt +++ /dev/null @@ -1,5 +0,0 @@ -Novità: -- Risolta regressione fermata tunnel -- Aggiunta offuscazione log -- Aggiunto nascondi FAB allo scorrimento -- Aggiunta localizzazione turca diff --git a/fastlane/metadata/android/it-IT/changelogs/34202.txt b/fastlane/metadata/android/it-IT/changelogs/34202.txt deleted file mode 100644 index ec485f39..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/34202.txt +++ /dev/null @@ -1,3 +0,0 @@ -Novità: -- Aggiunta Amnezia side-by-side con Wireguard -- Risolti problemi con le scorciatoie app diff --git a/fastlane/metadata/android/it-IT/changelogs/34300.txt b/fastlane/metadata/android/it-IT/changelogs/34300.txt deleted file mode 100644 index 6fddecb2..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/34300.txt +++ /dev/null @@ -1,6 +0,0 @@ -Novità: -- Supporto ufficiale per AmneziaWG -- Import/export per configurazioni Amenzia -- Commutazione Auto-tunnel una volta sola al cambio rete -- Supporto a lingue addizionali -- Altri problemi risolti e migliorie diff --git a/fastlane/metadata/android/it-IT/changelogs/34400.txt b/fastlane/metadata/android/it-IT/changelogs/34400.txt deleted file mode 100644 index f9aaa017..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/34400.txt +++ /dev/null @@ -1,5 +0,0 @@ -Novità: -- Migliorata nomenclatura importazione tunnel -- Risolto problema inizializzazione stato auto tunneling -- Migliorata gestione errori -- Risolto problema importazione zip Amnezia diff --git a/fastlane/metadata/android/it-IT/changelogs/34500.txt b/fastlane/metadata/android/it-IT/changelogs/34500.txt deleted file mode 100644 index e1da57ab..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/34500.txt +++ /dev/null @@ -1,5 +0,0 @@ -Novità: -- Supporto lingue aggiuntive -- Corretto problema auto-tunneling dati mobili -- Corretta azione pulsante mobile AndroidTV -- Altre ottimizzazioni e miglioramenti diff --git a/fastlane/metadata/android/it-IT/changelogs/34600.txt b/fastlane/metadata/android/it-IT/changelogs/34600.txt deleted file mode 100644 index a611a776..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/34600.txt +++ /dev/null @@ -1,4 +0,0 @@ -Novità: -- Corretti problemi auto tunneling -- Corretto problema backup android -- Aggiornate versioni diff --git a/fastlane/metadata/android/it-IT/changelogs/34700.txt b/fastlane/metadata/android/it-IT/changelogs/34700.txt deleted file mode 100644 index 0ebef6db..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/34700.txt +++ /dev/null @@ -1,6 +0,0 @@ -Novità: -- Corretti problemi di crash -- Migliorate le prestazioni -- Ri-abilita blocco pin -- Creata impostazione riavvia al boot -- Varie correzioni di problemi e di prestazioni diff --git a/fastlane/metadata/android/it-IT/changelogs/34800.txt b/fastlane/metadata/android/it-IT/changelogs/34800.txt deleted file mode 100644 index fee005b7..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/34800.txt +++ /dev/null @@ -1,5 +0,0 @@ -Novità: -- Correzioni per interfaccia controllo tunnel AndroidTV -- Correzioni problema blocco verticale -- Correzioni problema aggiramento blocco pin -- Correzioni problema tile auto tunnel diff --git a/fastlane/metadata/android/it-IT/changelogs/35000.txt b/fastlane/metadata/android/it-IT/changelogs/35000.txt deleted file mode 100644 index c08fcf44..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/35000.txt +++ /dev/null @@ -1,6 +0,0 @@ -Novità: -- Migliorata stabilità auto tunnel -- Aggiunto supporto per tema chiaro/scuro/dinamico -- Aggiunto supporto per script pre/post up/down -- Rimuovi tunnel notifica persistente -- Varie altre risoluzioni e migliorie diff --git a/fastlane/metadata/android/it-IT/changelogs/35100.txt b/fastlane/metadata/android/it-IT/changelogs/35100.txt deleted file mode 100644 index 66bd3614..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/35100.txt +++ /dev/null @@ -1,5 +0,0 @@ -Novità: -- Correzioni per tunnel che non si avviano dal background -- Aggiunto supporto per riavvio servizi dopo aggiornamento -- Migliorie velocità animazione UI -- Altre ottimizzazioni diff --git a/fastlane/metadata/android/it-IT/changelogs/35200.txt b/fastlane/metadata/android/it-IT/changelogs/35200.txt deleted file mode 100644 index 45d38ba9..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/35200.txt +++ /dev/null @@ -1,6 +0,0 @@ -Novità: -- Aggiunto supporto a caratteri jolly per nomi wifi -- Modifica live delle impostazioni del tunnel/auto-tunnel mentre attivo -- Risolta lentezza su dati mobili -- Varie risoluzioni di problemi e migliorie -- Ottimizzazioni UI diff --git a/fastlane/metadata/android/it-IT/changelogs/35300.txt b/fastlane/metadata/android/it-IT/changelogs/35300.txt deleted file mode 100644 index ed91f976..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/35300.txt +++ /dev/null @@ -1,6 +0,0 @@ -Novità: -- Dispositivi rootati ora acquisiscono nome wifi senza locazione -- Migliorie a scorrimento e condivisione schermata log -- Corretti problemi importazione tunne per AndroidTV 14 -- Miglioramenti UI statistiche tunnel -- Altre correzioni e migliorie diff --git a/fastlane/metadata/android/it-IT/changelogs/36000.txt b/fastlane/metadata/android/it-IT/changelogs/36000.txt deleted file mode 100644 index 492dea8a..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/36000.txt +++ /dev/null @@ -1,6 +0,0 @@ -Novità: -- aggiornamenti UI -- Migliorie navigazione AndroidTV -- Bug consumo batteria risolto -- Correzione impostazioni opzionali caratteri jolly -- Altre migliorie diff --git a/fastlane/metadata/android/it-IT/changelogs/36100.txt b/fastlane/metadata/android/it-IT/changelogs/36100.txt deleted file mode 100644 index 48b266fd..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/36100.txt +++ /dev/null @@ -1,7 +0,0 @@ -Novità: -- Aggiungi tunnel dagli appunti -- Aggiunte localizzazioni -- Corretto bug consumo batteria -- Corretto bug cancellazione -- Migliorata sincronizzazione tile tunnel -- Altre correzioni e migliorie diff --git a/fastlane/metadata/android/it-IT/changelogs/36200.txt b/fastlane/metadata/android/it-IT/changelogs/36200.txt deleted file mode 100644 index 33618bf3..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/36200.txt +++ /dev/null @@ -1,5 +0,0 @@ -Novità: -- Aggiunto interruttore spegnimento VPN con esclusione LAN -- Migliorata velocità e stabilità auto tunnel -- Migliorata sincronizzazione tile -- Vari bug risolti e migliorie diff --git a/fastlane/metadata/android/it-IT/changelogs/36300.txt b/fastlane/metadata/android/it-IT/changelogs/36300.txt deleted file mode 100644 index fd06bd75..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/36300.txt +++ /dev/null @@ -1,3 +0,0 @@ -Novità: -- Corretto bug commutatore modalità kernel -- Corretto bug crash notifica diff --git a/fastlane/metadata/android/it-IT/changelogs/36500.txt b/fastlane/metadata/android/it-IT/changelogs/36500.txt deleted file mode 100644 index 9b1a4fa1..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/36500.txt +++ /dev/null @@ -1,6 +0,0 @@ -Novità: -- Migliorata stabilità auto tunnel -- Corretto salvataggio split tunnel e migliore prestazioni -- Corretto bug modalità kernel -- Corretto bug tile -- Vare altre correzioni e migliorie diff --git a/fastlane/metadata/android/it-IT/changelogs/36600.txt b/fastlane/metadata/android/it-IT/changelogs/36600.txt deleted file mode 100644 index f6277909..00000000 --- a/fastlane/metadata/android/it-IT/changelogs/36600.txt +++ /dev/null @@ -1,6 +0,0 @@ -Novità: -- Il ping ora funziona indipendentemente dall'auto tunnel -- Aggiunte azioni per compatibilità Amnezia -- Aggiunte azioni esclusione LAN dal tunnel -- Aggiunta opzione regolazione tempo di rimbalzo per auto tunnel -- Molte correzioni a bug e migliore diff --git a/fastlane/metadata/android/ja-JP/changelogs/32100.txt b/fastlane/metadata/android/ja-JP/changelogs/32100.txt deleted file mode 100644 index 872fc7fc..00000000 --- a/fastlane/metadata/android/ja-JP/changelogs/32100.txt +++ /dev/null @@ -1,3 +0,0 @@ -機能強化 -- Android 9 のパーミッションのバグを修正 -- その他の最適化 diff --git a/fastlane/metadata/android/ja-JP/changelogs/32200.txt b/fastlane/metadata/android/ja-JP/changelogs/32200.txt deleted file mode 100644 index e96b7185..00000000 --- a/fastlane/metadata/android/ja-JP/changelogs/32200.txt +++ /dev/null @@ -1,5 +0,0 @@ -機能強化 -- メイン画面にトンネル統計を追加 -- 設定画面のAndroidTVナビゲーションを改善 -- 通知バイブレーションの削除 -- その他様々なバグ修正 diff --git a/fastlane/metadata/android/ja-JP/changelogs/32300.txt b/fastlane/metadata/android/ja-JP/changelogs/32300.txt deleted file mode 100644 index dc504d6b..00000000 --- a/fastlane/metadata/android/ja-JP/changelogs/32300.txt +++ /dev/null @@ -1,5 +0,0 @@ -機能強化 -- モバイルデータのみのオートトンネルのサポートを追加 -- サポート画面のUI改善 -- リソースリンクの更新 -- その他様々なバグ修正 diff --git a/fastlane/metadata/android/ja-JP/changelogs/32400.txt b/fastlane/metadata/android/ja-JP/changelogs/32400.txt deleted file mode 100644 index 25f9308a..00000000 --- a/fastlane/metadata/android/ja-JP/changelogs/32400.txt +++ /dev/null @@ -1,5 +0,0 @@ -機能強化 -- WireGuard Kernelの基本サポートの追加 -- 位置情報開示フローの改善 -- オートトンネルでのパーミッションのバグの修正 -- その他のさまざまな UI バグの修正 diff --git a/fastlane/metadata/android/ja-JP/changelogs/32500.txt b/fastlane/metadata/android/ja-JP/changelogs/32500.txt deleted file mode 100644 index 17aa1b6f..00000000 --- a/fastlane/metadata/android/ja-JP/changelogs/32500.txt +++ /dev/null @@ -1,2 +0,0 @@ -修正 -- Android 14のフォアグラウンドパーミッション diff --git a/fastlane/metadata/android/nl-NL/changelogs/32100.txt b/fastlane/metadata/android/nl-NL/changelogs/32100.txt deleted file mode 100644 index 04e9e567..00000000 --- a/fastlane/metadata/android/nl-NL/changelogs/32100.txt +++ /dev/null @@ -1,3 +0,0 @@ -Verbeteringen: -- Fix < Android 9 permission bug -- Andere optimalisaties diff --git a/fastlane/metadata/android/nl-NL/changelogs/32200.txt b/fastlane/metadata/android/nl-NL/changelogs/32200.txt deleted file mode 100644 index f8bfa5e3..00000000 --- a/fastlane/metadata/android/nl-NL/changelogs/32200.txt +++ /dev/null @@ -1,5 +0,0 @@ -Verbeteringen: -- Tunnelstatistieken toevoegen aan hoofdscherm -- Instellingenscherm AndroidTV-navigatie verbeteren -- Meldingsvibratie verwijderen -- Diverse andere bugfixes diff --git a/fastlane/metadata/android/pl-PL/changelogs/32100.txt b/fastlane/metadata/android/pl-PL/changelogs/32100.txt deleted file mode 100644 index 2d73f6ff..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/32100.txt +++ /dev/null @@ -1,3 +0,0 @@ -Ulepszenia: -- Naprawiono < błąd uprawnień Androida 9 -- Inne optymalizacje diff --git a/fastlane/metadata/android/pl-PL/changelogs/32200.txt b/fastlane/metadata/android/pl-PL/changelogs/32200.txt deleted file mode 100644 index d003a085..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/32200.txt +++ /dev/null @@ -1,5 +0,0 @@ -Ulepszenia: -- Dodanie statystyk tunelu do ekranu głównego -- Ulepszenie nawigacji AndroidTV na ekranie ustawień -- Usunięcie wibracji powiadomień -- Różne inne poprawki błędów diff --git a/fastlane/metadata/android/pl-PL/changelogs/32300.txt b/fastlane/metadata/android/pl-PL/changelogs/32300.txt deleted file mode 100644 index 14b7a69b..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/32300.txt +++ /dev/null @@ -1,5 +0,0 @@ -Ulepszenia: -- Dodano obsługę automatycznego tunelowania tylko dla danych mobilnych -- Ulepszono interfejs użytkownika ekranu pomocy technicznej -- Zaktualizowano łącza do zasobów -- Różne inne poprawki błędów diff --git a/fastlane/metadata/android/pl-PL/changelogs/32400.txt b/fastlane/metadata/android/pl-PL/changelogs/32400.txt deleted file mode 100644 index 970d2866..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/32400.txt +++ /dev/null @@ -1,5 +0,0 @@ -Ulepszenia: -- Dodano podstawową obsługę jądra protokołu WireGuard -- Ulepszony przepływ ujawniania lokalizacji -- Naprawiono błąd uprawnień automatycznego tunelowania -- Różne inne poprawki błędów interfejsu użytkownika diff --git a/fastlane/metadata/android/pl-PL/changelogs/32500.txt b/fastlane/metadata/android/pl-PL/changelogs/32500.txt deleted file mode 100644 index f65e5733..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/32500.txt +++ /dev/null @@ -1,2 +0,0 @@ -Poprawki: -- Uprawnienia do pierwszego planu w systemie Android 14 diff --git a/fastlane/metadata/android/pl-PL/changelogs/33000.txt b/fastlane/metadata/android/pl-PL/changelogs/33000.txt deleted file mode 100644 index 2d8942e8..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/33000.txt +++ /dev/null @@ -1,7 +0,0 @@ -Ulepszenia: -- Refaktoryzacja zarządzania stanem -- Ulepszenie nawigacji AndroidTV -- Ulepszenie wydajności automatycznego tunelowania -- Ulepszenie nawigacji -- Funkcja pauzy automatycznego tunelowania -- Wiele poprawek błędów diff --git a/fastlane/metadata/android/pl-PL/changelogs/33100.txt b/fastlane/metadata/android/pl-PL/changelogs/33100.txt deleted file mode 100644 index 2d8942e8..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/33100.txt +++ /dev/null @@ -1,7 +0,0 @@ -Ulepszenia: -- Refaktoryzacja zarządzania stanem -- Ulepszenie nawigacji AndroidTV -- Ulepszenie wydajności automatycznego tunelowania -- Ulepszenie nawigacji -- Funkcja pauzy automatycznego tunelowania -- Wiele poprawek błędów diff --git a/fastlane/metadata/android/pl-PL/changelogs/33200.txt b/fastlane/metadata/android/pl-PL/changelogs/33200.txt deleted file mode 100644 index 7c3be9bf..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/33200.txt +++ /dev/null @@ -1,8 +0,0 @@ -Ulepszenia: -- Refaktoryzacja zarządzania stanem -- Poprawa nawigacji AndroidTV -- Poprawa wydajności automatycznego tunelowania -- Poprawa nawigacji -- Funkcja pauzy automatycznego tunelowania -- Naprawiono autotunelowanie uruchamiania pierwszego planu -- Wiele poprawek błędów diff --git a/fastlane/metadata/android/pl-PL/changelogs/33300.txt b/fastlane/metadata/android/pl-PL/changelogs/33300.txt deleted file mode 100644 index a1e62b4a..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/33300.txt +++ /dev/null @@ -1,7 +0,0 @@ -Ulepszenia: -- Dodano potwierdzenie usunięcia tunelu -- Dodano uprawnienia w tle dla trybu oszczędzania energii -Poprawki: -- Błąd wyłączania tunelu zamrożonego -- Błąd w polu odbiorcy wiadomości e-mail -- Edycja konfiguracji pustego DNS diff --git a/fastlane/metadata/android/pl-PL/changelogs/33400.txt b/fastlane/metadata/android/pl-PL/changelogs/33400.txt deleted file mode 100644 index d369eb90..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/33400.txt +++ /dev/null @@ -1,3 +0,0 @@ -Ulepszenia: -- Naprawiono niezapisywanie konfiguracji tworzenia -- Podniesiono wersje diff --git a/fastlane/metadata/android/pl-PL/changelogs/33401.txt b/fastlane/metadata/android/pl-PL/changelogs/33401.txt deleted file mode 100644 index dfb6b76c..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/33401.txt +++ /dev/null @@ -1,2 +0,0 @@ -Co nowego: -- To jest wersja testowa CI diff --git a/fastlane/metadata/android/pl-PL/changelogs/33500.txt b/fastlane/metadata/android/pl-PL/changelogs/33500.txt deleted file mode 100644 index 2bdeb567..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/33500.txt +++ /dev/null @@ -1,5 +0,0 @@ -Co nowego: -- Automatyczne uruchamianie po ponownym uruchomieniu w trybie jądra stałego VPN -- Obsługa ikon adaptacyjnych motywów -- Naprawiono ikony powiadomień, ikonę kafelka -- Naprawiono ikony AndroidTV diff --git a/fastlane/metadata/android/pl-PL/changelogs/33600.txt b/fastlane/metadata/android/pl-PL/changelogs/33600.txt deleted file mode 100644 index 41c3181d..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/33600.txt +++ /dev/null @@ -1,5 +0,0 @@ -Co nowego: -- Ulepszono przepływ pierwszego uruchomienia -- Przejście na fork biblioteki protokołu Wireguard -- Żądanie zezwolenia VPN przy pierwszym uruchomieniu VPN -- Podniesienie wersji diff --git a/fastlane/metadata/android/pl-PL/changelogs/33700.txt b/fastlane/metadata/android/pl-PL/changelogs/33700.txt deleted file mode 100644 index a7832af3..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/33700.txt +++ /dev/null @@ -1,2 +0,0 @@ -Co nowego: -- Naprawiono błąd interfejsu użytkownika wyświetlania tunelu diff --git a/fastlane/metadata/android/pl-PL/changelogs/33800.txt b/fastlane/metadata/android/pl-PL/changelogs/33800.txt deleted file mode 100644 index 038b50dd..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/33800.txt +++ /dev/null @@ -1,4 +0,0 @@ -Co nowego: -- Naprawiono błąd w interfejsie użytkownika edycji konfiguracji -- Dodano powiadomienie o pierwszym uruchomieniu AOVPN systemu GrapheneOS -- Podniesienie wersji diff --git a/fastlane/metadata/android/pl-PL/changelogs/33900.txt b/fastlane/metadata/android/pl-PL/changelogs/33900.txt deleted file mode 100644 index d297787e..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/33900.txt +++ /dev/null @@ -1,5 +0,0 @@ -Co nowego: -- Dodano ekran dzienników -- Dodano blokadę aplikacji lokalnej -- Dodano ponowne uruchomienie VPN w przypadku nieudanego pingowania -- Różne poprawki błędów diff --git a/fastlane/metadata/android/pl-PL/changelogs/34000.txt b/fastlane/metadata/android/pl-PL/changelogs/34000.txt deleted file mode 100644 index 5b333df2..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/34000.txt +++ /dev/null @@ -1,5 +0,0 @@ -Co nowego: -- Automatyczne tunelowanie do określonych tuneli według nazwy Wi-Fi -- Automatyczne sterowanie tunelem z kafelka i skrótów -- Automatyczne uruchamianie ręcznego tunelu, jeśli był włączony przed ponownym uruchomieniem -- Różne poprawki błędów i ulepszenia wydajności diff --git a/fastlane/metadata/android/pl-PL/changelogs/34100.txt b/fastlane/metadata/android/pl-PL/changelogs/34100.txt deleted file mode 100644 index 15cdc993..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/34100.txt +++ /dev/null @@ -1,5 +0,0 @@ -Co nowego: -- Poprawiona niezawodność automatycznego tunelowania -- Poprawiona synchronizacja kafelków -- Dodano zasoby AndroidTV -- Dodano odcisk palca APK diff --git a/fastlane/metadata/android/pl-PL/changelogs/34200.txt b/fastlane/metadata/android/pl-PL/changelogs/34200.txt deleted file mode 100644 index d5ce44fe..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/34200.txt +++ /dev/null @@ -1,5 +0,0 @@ -Co nowego: -- Naprawiono regresję zatrzymania tunelu -- Dodano zaciemnianie dzienników -- Dodano ukrywanie FAB podczas przewijania -- Dodano lokalizację turecką diff --git a/fastlane/metadata/android/pl-PL/changelogs/34202.txt b/fastlane/metadata/android/pl-PL/changelogs/34202.txt deleted file mode 100644 index f89d9e8d..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/34202.txt +++ /dev/null @@ -1,3 +0,0 @@ -Co nowego: -- Dodano protokołu Amnezia obok protokołu WireGuard -- Naprawiono błąd skrótów aplikacji diff --git a/fastlane/metadata/android/pl-PL/changelogs/34300.txt b/fastlane/metadata/android/pl-PL/changelogs/34300.txt deleted file mode 100644 index 44229501..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/34300.txt +++ /dev/null @@ -1,6 +0,0 @@ -Co nowego: -- Oficjalne wsparcie dla protokołu AmneziaWG -- Importowanie i eksportowanie konfiguracji protokołu Amnezia -- Autotunel umożliwiający przełączanie tylko raz przy każdej zmianie sieci -- Obsługa dodatkowych języków -- Inne poprawki błędów i ulepszenia diff --git a/fastlane/metadata/android/pl-PL/changelogs/34400.txt b/fastlane/metadata/android/pl-PL/changelogs/34400.txt deleted file mode 100644 index d0354e13..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/34400.txt +++ /dev/null @@ -1,5 +0,0 @@ -Co nowego: -- Ulepszono nazewnictwo importowania tunelu -- Naprawiono błąd stanu inicjalizacji automatycznego tunelowania -- Ulepszono obsługę błędów -- Naprawiono błąd importu ZIP protokołu Amnezia diff --git a/fastlane/metadata/android/pl-PL/changelogs/34500.txt b/fastlane/metadata/android/pl-PL/changelogs/34500.txt deleted file mode 100644 index bf7157f2..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/34500.txt +++ /dev/null @@ -1,5 +0,0 @@ -Co nowego: -- Dodatkowe wsparcie językowe -- Naprawiono błąd automatycznego tunelowania danych mobilnych -- Naprawiono pływający przycisk akcji AndroidTV -- Inne optymalizacje i ulepszenia diff --git a/fastlane/metadata/android/pl-PL/changelogs/34600.txt b/fastlane/metadata/android/pl-PL/changelogs/34600.txt deleted file mode 100644 index 398295ad..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/34600.txt +++ /dev/null @@ -1,4 +0,0 @@ -Co nowego: -- Naprawiono błędy automatycznego tunelowania -- Naprawiono błąd kopii zapasowej Androida -- Podniesienie wersji diff --git a/fastlane/metadata/android/pl-PL/changelogs/34700.txt b/fastlane/metadata/android/pl-PL/changelogs/34700.txt deleted file mode 100644 index 0079101e..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/34700.txt +++ /dev/null @@ -1,6 +0,0 @@ -Co nowego: -- Naprawiono problemy z awariami -- Poprawiono wydajność kafelków -- Ponownie włączono blokadę PIN -- Ustawiono opcję ponownego uruchomienia przy rozruchu -- Różne poprawki wydajności i błędów diff --git a/fastlane/metadata/android/pl-PL/changelogs/34800.txt b/fastlane/metadata/android/pl-PL/changelogs/34800.txt deleted file mode 100644 index b05373eb..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/34800.txt +++ /dev/null @@ -1,5 +0,0 @@ -Co nowego: -- Naprawiono sterowanie tunelem w interfejsie AndroidTV -- Naprawiono błąd blokady portretu -- Naprawiono błąd pomijania blokady PIN -- Naprawiono błąd automatycznego tunelowania kafelków diff --git a/fastlane/metadata/android/pl-PL/changelogs/35000.txt b/fastlane/metadata/android/pl-PL/changelogs/35000.txt deleted file mode 100644 index f1e43751..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/35000.txt +++ /dev/null @@ -1,6 +0,0 @@ -Co nowego: -- Poprawa niezawodności automatycznego tunelowania -- Dodanie obsługi jasnych/ciemnych/dynamicznych motywów -- Dodanie obsługi skryptów przed/po włączeniu/wyłącz -- Usunięcie trwałego powiadomienia o tunelu -- Różne inne poprawki i ulepszenia diff --git a/fastlane/metadata/android/pl-PL/changelogs/35100.txt b/fastlane/metadata/android/pl-PL/changelogs/35100.txt deleted file mode 100644 index 435f2adc..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/35100.txt +++ /dev/null @@ -1,5 +0,0 @@ -Co nowego: -- Poprawki dla tuneli, które nie uruchamiają się w tle -- Dodano obsługę ponownego uruchamiania usług po aktualizacji -- Poprawki szybkości animacji interfejsu użytkownika -- Inne optymalizacje diff --git a/fastlane/metadata/android/pl-PL/changelogs/35200.txt b/fastlane/metadata/android/pl-PL/changelogs/35200.txt deleted file mode 100644 index cd015471..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/35200.txt +++ /dev/null @@ -1,6 +0,0 @@ -Co nowego: -- Dodano obsługę symboli wieloznacznych dla nazw sieci Wi-Fi -- Edycja na żywo ustawień tunelu/autotunelowania podczas aktywności -- Naprawiono powolność danych mobilnych -- Różne poprawki błędów i ulepszenia -- Optymalizacje interfejsu użytkownika diff --git a/fastlane/metadata/android/pl-PL/changelogs/35300.txt b/fastlane/metadata/android/pl-PL/changelogs/35300.txt deleted file mode 100644 index 7125813e..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/35300.txt +++ /dev/null @@ -1,6 +0,0 @@ -Co nowego: -- Zrootowane urządzenia otrzymują teraz nazwę Wi-Fi bez lokalizacji -- Ulepszenia przewijania ekranu dzienników i udostępniania -- Poprawki błędów importu tunelu dla AndroidTV 14 -- Ulepszenia interfejsu użytkownika statystyk tunelu -- Inne poprawki błędów i ulepszenia diff --git a/fastlane/metadata/android/pl-PL/changelogs/36000.txt b/fastlane/metadata/android/pl-PL/changelogs/36000.txt deleted file mode 100644 index c04582d3..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/36000.txt +++ /dev/null @@ -1,6 +0,0 @@ -Co nowego: -- Aktualizacja interfejsu użytkownika -- Ulepszenia nawigacji AndroidTV -- Naprawiono błąd rozładowania akumulatora -- Naprawiono symbole wieloznaczne z opcjonalnym ustawieniem -- Inne ulepszenia diff --git a/fastlane/metadata/android/pl-PL/changelogs/36100.txt b/fastlane/metadata/android/pl-PL/changelogs/36100.txt deleted file mode 100644 index 096bfe8b..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/36100.txt +++ /dev/null @@ -1,7 +0,0 @@ -Co nowego: -- Dodano tunel ze schowka -- Dodano tłumaczenia -- Naprawiono błąd rozładowywania akumulatora -- Naprawiono błąd usuwania -- Ulepszono synchronizację kafelków tunelu -- Inne poprawki i ulepszenia diff --git a/fastlane/metadata/android/pl-PL/changelogs/36200.txt b/fastlane/metadata/android/pl-PL/changelogs/36200.txt deleted file mode 100644 index f233a29a..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/36200.txt +++ /dev/null @@ -1,5 +0,0 @@ -Co nowego: -- Dodano wyłącznik awaryjny VPN z pominięciem LAN -- Ulepszono prędkość i niezawodność automatycznego tunelowania -- Ulepszono kafelki synchronizacji -- Różne poprawki błędów i ulepszenia diff --git a/fastlane/metadata/android/pl-PL/changelogs/36300.txt b/fastlane/metadata/android/pl-PL/changelogs/36300.txt deleted file mode 100644 index 49f016da..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/36300.txt +++ /dev/null @@ -1,3 +0,0 @@ -Co nowego: -- Naprawiono błąd przełączania trybu jądra -- Naprawiono błąd awarii powiadomień diff --git a/fastlane/metadata/android/pl-PL/changelogs/36500.txt b/fastlane/metadata/android/pl-PL/changelogs/36500.txt deleted file mode 100644 index bab3d326..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/36500.txt +++ /dev/null @@ -1,6 +0,0 @@ -Co nowego: -- Poprawki niezawodności automatycznego tunelowania -- Naprawiono zapis podzielonego tunelu i poprawiono wydajności -- Naprawiono błąd trybu jądra -- Naprawiono błąd kafelków -- Różne inne poprawki i ulepszenia diff --git a/fastlane/metadata/android/pl-PL/changelogs/36600.txt b/fastlane/metadata/android/pl-PL/changelogs/36600.txt deleted file mode 100644 index f21c0e9c..00000000 --- a/fastlane/metadata/android/pl-PL/changelogs/36600.txt +++ /dev/null @@ -1,6 +0,0 @@ -Co nowego: -- Funkcja ping działa teraz niezależnie od autotunelowania -- Dodano wygodną czynność dla zgodności z protokołem Amnezia -- Dodano wygodną czynność dla wykluczenia sieci LAN z tunelowania -- Dodano opcję dostrajania opóźnienia odbicia dla autotunelowania -- Wiele poprawek błędów i ulepszeń diff --git a/fastlane/metadata/android/pt-BR/changelogs/32100.txt b/fastlane/metadata/android/pt-BR/changelogs/32100.txt deleted file mode 100644 index 77848d94..00000000 --- a/fastlane/metadata/android/pt-BR/changelogs/32100.txt +++ /dev/null @@ -1,3 +0,0 @@ -Melhorias: -- Corrige o bug de permissões do Android 9 -- Outras otimizações diff --git a/fastlane/metadata/android/pt-BR/changelogs/32200.txt b/fastlane/metadata/android/pt-BR/changelogs/32200.txt deleted file mode 100644 index dcaca201..00000000 --- a/fastlane/metadata/android/pt-BR/changelogs/32200.txt +++ /dev/null @@ -1,5 +0,0 @@ -Melhorias: -- Adicionada estatísticas do túnel no ecrã principal -- Melhoria de navegação de configurações no ecrã do AndroidTV -- Removida a vibração nas notificações -- Outras correções de bugs diff --git a/fastlane/metadata/android/pt-PT/changelogs/32100.txt b/fastlane/metadata/android/pt-PT/changelogs/32100.txt deleted file mode 100644 index 77848d94..00000000 --- a/fastlane/metadata/android/pt-PT/changelogs/32100.txt +++ /dev/null @@ -1,3 +0,0 @@ -Melhorias: -- Corrige o bug de permissões do Android 9 -- Outras otimizações diff --git a/fastlane/metadata/android/pt-PT/changelogs/32200.txt b/fastlane/metadata/android/pt-PT/changelogs/32200.txt deleted file mode 100644 index dcaca201..00000000 --- a/fastlane/metadata/android/pt-PT/changelogs/32200.txt +++ /dev/null @@ -1,5 +0,0 @@ -Melhorias: -- Adicionada estatísticas do túnel no ecrã principal -- Melhoria de navegação de configurações no ecrã do AndroidTV -- Removida a vibração nas notificações -- Outras correções de bugs diff --git a/fastlane/metadata/android/ru-RU/changelogs/32100.txt b/fastlane/metadata/android/ru-RU/changelogs/32100.txt deleted file mode 100644 index 86e57030..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/32100.txt +++ /dev/null @@ -1,3 +0,0 @@ -Улучшения: -- Исправлена ошибка с разрешениями на Android < 9 -- Другие оптимизации diff --git a/fastlane/metadata/android/ru-RU/changelogs/32200.txt b/fastlane/metadata/android/ru-RU/changelogs/32200.txt deleted file mode 100644 index 6c4e141c..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/32200.txt +++ /dev/null @@ -1,5 +0,0 @@ -Улучшения: -- Добавлена статистики туннеля на главный экран -- Улучшена навигация в экране настроек на Android TV -- Удалена вибрация при оповещении -- Другие исправления diff --git a/fastlane/metadata/android/ru-RU/changelogs/32300.txt b/fastlane/metadata/android/ru-RU/changelogs/32300.txt deleted file mode 100644 index a55016a6..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/32300.txt +++ /dev/null @@ -1,5 +0,0 @@ -Улучшения: -- Добавлена поддержка авто-туннелирования только через мобильную сеть -- Улучшен интерфейс экрана поддержки -- Обновлены ссылки на ресурсы -- Прочие исправления ошибок diff --git a/fastlane/metadata/android/ru-RU/changelogs/32400.txt b/fastlane/metadata/android/ru-RU/changelogs/32400.txt deleted file mode 100644 index b31706f4..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/32400.txt +++ /dev/null @@ -1,5 +0,0 @@ -Улучшения: -- Базовая поддержка модуля WireGuard режима ядра -- Улучшен процесс раскрытия информации о местоположении -- Исправлена ошибка с разрешениями авто-туннеля -- Прочие исправления интерфейса diff --git a/fastlane/metadata/android/ru-RU/changelogs/32500.txt b/fastlane/metadata/android/ru-RU/changelogs/32500.txt deleted file mode 100644 index d8da8c97..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/32500.txt +++ /dev/null @@ -1,2 +0,0 @@ -Исправления: -- Разрешения переднего плана в Android 14 diff --git a/fastlane/metadata/android/ru-RU/changelogs/33000.txt b/fastlane/metadata/android/ru-RU/changelogs/33000.txt deleted file mode 100644 index d0f0a621..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/33000.txt +++ /dev/null @@ -1,7 +0,0 @@ -Улучшения: -- Рефакторинг хранения данных приложением -- Улучшена навигация в интерфейсе для Android TV -- Увеличена эффективность авто-туннелирования -- Улучшена навигация -- Возможность приостановить работу авто-туннеля -- Множество исправлений ошибок diff --git a/fastlane/metadata/android/ru-RU/changelogs/33100.txt b/fastlane/metadata/android/ru-RU/changelogs/33100.txt deleted file mode 100644 index d0f0a621..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/33100.txt +++ /dev/null @@ -1,7 +0,0 @@ -Улучшения: -- Рефакторинг хранения данных приложением -- Улучшена навигация в интерфейсе для Android TV -- Увеличена эффективность авто-туннелирования -- Улучшена навигация -- Возможность приостановить работу авто-туннеля -- Множество исправлений ошибок diff --git a/fastlane/metadata/android/ru-RU/changelogs/33200.txt b/fastlane/metadata/android/ru-RU/changelogs/33200.txt deleted file mode 100644 index 78e9953a..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/33200.txt +++ /dev/null @@ -1,8 +0,0 @@ -Улучшения: -- Рефакторинг хранения данных приложением -- Улучшена навигация в интерфейсе для Android TV -- Увеличена эффективность авто-туннелирования -- Улучшена навигация -- Возможность приостановить работу авто-туннеля -- Исправлен запуск авто-туннеля на переднем плане -- Множество исправлений ошибок diff --git a/fastlane/metadata/android/ru-RU/changelogs/33300.txt b/fastlane/metadata/android/ru-RU/changelogs/33300.txt deleted file mode 100644 index b1cc3256..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/33300.txt +++ /dev/null @@ -1,7 +0,0 @@ -Улучшения: -- Добавлено подтверждение при удалении туннеля -- Добавлено разрешение на работу в фоне -Исправления: -- Зависание приложения при отключении туннеля -- Ошибка в поле получателя электронной почты -- Редактирование конфигурации с пустым полем DNS diff --git a/fastlane/metadata/android/ru-RU/changelogs/33400.txt b/fastlane/metadata/android/ru-RU/changelogs/33400.txt deleted file mode 100644 index 89d1ffbf..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/33400.txt +++ /dev/null @@ -1,3 +0,0 @@ -Улучшения: -- Исправлен сбой сохранения созданных настроек -- Увеличен номер версии diff --git a/fastlane/metadata/android/ru-RU/changelogs/33401.txt b/fastlane/metadata/android/ru-RU/changelogs/33401.txt deleted file mode 100644 index 3829910d..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/33401.txt +++ /dev/null @@ -1,2 +0,0 @@ -Что нового: -- Тестовая версия для отладки Continuous Integration diff --git a/fastlane/metadata/android/ru-RU/changelogs/33500.txt b/fastlane/metadata/android/ru-RU/changelogs/33500.txt deleted file mode 100644 index 836fb307..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/33500.txt +++ /dev/null @@ -1,5 +0,0 @@ -Что нового: -- Автозапуск «Всегда включенного» VPN режима ядра при перезагрузке системы -- Поддержка значков адаптивной темы -- Исправлены значки уведомлений и плитки -- Исправлены значки Android TV diff --git a/fastlane/metadata/android/ru-RU/changelogs/33600.txt b/fastlane/metadata/android/ru-RU/changelogs/33600.txt deleted file mode 100644 index 2997152e..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/33600.txt +++ /dev/null @@ -1,5 +0,0 @@ -Что нового: -- Улучшен процесс первого запуска приложения -- Переход на альтернативную реализацию библиотеки WireGuard -- Запрос разрешения на доступ к VPN при первом подключении VPN -- Увеличен номер версии diff --git a/fastlane/metadata/android/ru-RU/changelogs/33700.txt b/fastlane/metadata/android/ru-RU/changelogs/33700.txt deleted file mode 100644 index 21b650c7..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/33700.txt +++ /dev/null @@ -1,2 +0,0 @@ -Что нового: -- Исправлена ошибка в интерфейсе туннеля diff --git a/fastlane/metadata/android/ru-RU/changelogs/33800.txt b/fastlane/metadata/android/ru-RU/changelogs/33800.txt deleted file mode 100644 index dbb1b308..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/33800.txt +++ /dev/null @@ -1,4 +0,0 @@ -Что нового: -- Исправлена ошибка в интерфейсе редактора конфигурации -- Добавлено уведомление AOVPN при первом запуске на Graphene OS -- Увеличен номер версии diff --git a/fastlane/metadata/android/ru-RU/changelogs/33900.txt b/fastlane/metadata/android/ru-RU/changelogs/33900.txt deleted file mode 100644 index b59fcbfe..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/33900.txt +++ /dev/null @@ -1,5 +0,0 @@ -Что нового: -- Добавлен экран с журналами выполнения -- Добавлена блокировка приложения -- Добавлен перезапуск туннеля при сбое пинга -- Различные исправления ошибок diff --git a/fastlane/metadata/android/ru-RU/changelogs/34000.txt b/fastlane/metadata/android/ru-RU/changelogs/34000.txt deleted file mode 100644 index a759309f..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/34000.txt +++ /dev/null @@ -1,5 +0,0 @@ -Что нового: -- Авто-туннель в зависимости от имени сети Wi-Fi -- Управление авто-туннелем через плитку и ярлыки -- Автозапуск туннелей, активных на момент перезагрузки -- Прочие исправления и улучшения производительности diff --git a/fastlane/metadata/android/ru-RU/changelogs/34100.txt b/fastlane/metadata/android/ru-RU/changelogs/34100.txt deleted file mode 100644 index 82a3ba88..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/34100.txt +++ /dev/null @@ -1,5 +0,0 @@ -Что нового: -- Улучшена надёжность работы авто-туннеля -- Улучшена синхронизация плитки -- Добавлены ресурсы для интерфейса Android TV -- Добавлен отпечаток APK diff --git a/fastlane/metadata/android/ru-RU/changelogs/34200.txt b/fastlane/metadata/android/ru-RU/changelogs/34200.txt deleted file mode 100644 index 737bc12e..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/34200.txt +++ /dev/null @@ -1,5 +0,0 @@ -Что нового: -- Исправлена регрессия с остановкой туннеля -- Добавлена обфускация лог-файлов -- Скрытие плавающей кнопки при прокрутке экрана -- Добавлен перевод на турецкий diff --git a/fastlane/metadata/android/ru-RU/changelogs/34202.txt b/fastlane/metadata/android/ru-RU/changelogs/34202.txt deleted file mode 100644 index 903771b2..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/34202.txt +++ /dev/null @@ -1,3 +0,0 @@ -Что нового: -- Добавление профилей Amnezia бок о бок с WireGuard -- Исправлена ошибка с ярлыками приложения diff --git a/fastlane/metadata/android/ru-RU/changelogs/34300.txt b/fastlane/metadata/android/ru-RU/changelogs/34300.txt deleted file mode 100644 index c29fffb4..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/34300.txt +++ /dev/null @@ -1,6 +0,0 @@ -Что нового: -- Официальная поддержка AmneziaWG -- Импорт/экспорт конфигурации AmneziaWG -- Однократное переключение авто-туннеля при изменении состояния сети -- Поддержка новых языков -- Прочие исправления и улучшения diff --git a/fastlane/metadata/android/ru-RU/changelogs/34400.txt b/fastlane/metadata/android/ru-RU/changelogs/34400.txt deleted file mode 100644 index 710c78f7..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/34400.txt +++ /dev/null @@ -1,5 +0,0 @@ -Что нового: -- Улучшено назначение имён при импорте туннелей -- Исправлена ошибка начального состояния авто-туннеля -- Улучшена обработка ошибок -- Исправлена ошибка импорта Amnezia из .zip diff --git a/fastlane/metadata/android/ru-RU/changelogs/34500.txt b/fastlane/metadata/android/ru-RU/changelogs/34500.txt deleted file mode 100644 index ed1998e3..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/34500.txt +++ /dev/null @@ -1,5 +0,0 @@ -Что нового: -- Добавлены новые переводы -- Исправлена работа авто-туннеля на мобильном интернете -- Исправлена проблема с плавающей кнопкой на Android TV -- Другие оптимизации и улучшения diff --git a/fastlane/metadata/android/ru-RU/changelogs/34600.txt b/fastlane/metadata/android/ru-RU/changelogs/34600.txt deleted file mode 100644 index a12d254c..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/34600.txt +++ /dev/null @@ -1,4 +0,0 @@ -Что нового: -- Исправлены проблемы с работой авто-туннеля -- Исправлена проблема с резервным копированием Android -- Увеличен номер версии diff --git a/fastlane/metadata/android/ru-RU/changelogs/34700.txt b/fastlane/metadata/android/ru-RU/changelogs/34700.txt deleted file mode 100644 index d2da54a3..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/34700.txt +++ /dev/null @@ -1,6 +0,0 @@ -Что нового: -- Исправлены проблемы со сбоями -- Улучшена производительность плиток -- Возвращена блокировка PIN-кодом -- Добавлена функция перезапуска при загрузке -- Различные исправления производительности и ошибок diff --git a/fastlane/metadata/android/ru-RU/changelogs/34800.txt b/fastlane/metadata/android/ru-RU/changelogs/34800.txt deleted file mode 100644 index 2180cbff..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/34800.txt +++ /dev/null @@ -1,5 +0,0 @@ -Что нового: -- Исправления интерфейса управления туннелем на AndroidTV -- Исправление ошибки блокировки в портретной ориентации -- Исправлена ошибка обхода блокировки PIN-кодом -- Исправлена ошибка с плиткой авто-туннеля diff --git a/fastlane/metadata/android/ru-RU/changelogs/35000.txt b/fastlane/metadata/android/ru-RU/changelogs/35000.txt deleted file mode 100644 index 61359c84..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/35000.txt +++ /dev/null @@ -1,6 +0,0 @@ -Что нового: -- Повышена надежность авто-туннелирования -- Добавлена поддержка светлых/темных/динамических тем -- Добавлена поддержка сценариев до/после запуска/остановки интерфейса -- Удалено постоянное уведомление о туннеле -- Различные другие исправления и улучшения diff --git a/fastlane/metadata/android/ru-RU/changelogs/35100.txt b/fastlane/metadata/android/ru-RU/changelogs/35100.txt deleted file mode 100644 index f6d7b291..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/35100.txt +++ /dev/null @@ -1,5 +0,0 @@ -Что нового: -- Исправлены ошибки, из-за которых туннели не запускались в фоновом режиме -- Добавлена поддержка перезапуска служб после обновления -- Улучшена скорость анимации интерфейса -- Другие оптимизации diff --git a/fastlane/metadata/android/ru-RU/changelogs/35200.txt b/fastlane/metadata/android/ru-RU/changelogs/35200.txt deleted file mode 100644 index d3eb89a9..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/35200.txt +++ /dev/null @@ -1,6 +0,0 @@ -Что нового: -- Добавлена поддержка подстановочных знаков для имён сетей Wi-Fi -- Редактирование настроек туннеля/автотуннеля прямо во время его работы -- Исправлено замедление передачи данных в мобильных сетях -- Различные исправления ошибок и улучшения -- Оптимизация пользовательского интерфейса diff --git a/fastlane/metadata/android/ru-RU/changelogs/35300.txt b/fastlane/metadata/android/ru-RU/changelogs/35300.txt deleted file mode 100644 index b7ce48ab..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/35300.txt +++ /dev/null @@ -1,6 +0,0 @@ -Что нового: -- Устройства с root-доступом теперь получают имя Wi-Fi без использования местоположения -- Улучшения прокрутки экрана журналов и обмена -- Исправление ошибок импорта туннелей для AndroidTV 14 -- Улучшения интерфейса статистики туннелей -- Другие исправления ошибок и улучшения diff --git a/fastlane/metadata/android/ru-RU/changelogs/36000.txt b/fastlane/metadata/android/ru-RU/changelogs/36000.txt deleted file mode 100644 index b3207a12..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/36000.txt +++ /dev/null @@ -1,6 +0,0 @@ -Что нового: -- Обновление интерфейса -- Улучшения навигации AndroidTV -- Исправление ошибки разряда батареи -- Исправление подстановочных знаков дополнительной настройкой -- Другие улучшения diff --git a/fastlane/metadata/android/ru-RU/changelogs/36100.txt b/fastlane/metadata/android/ru-RU/changelogs/36100.txt deleted file mode 100644 index 7ba41f2f..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/36100.txt +++ /dev/null @@ -1,7 +0,0 @@ -Что нового: -- Добавление туннеля из буфера обмена -- Добавление переводов -- Исправлена ошибка разряда батареи -- Исправлена ошибка удаления -- Улучшена синхронизация плиток туннеля -- Другие исправления и улучшения diff --git a/fastlane/metadata/android/ru-RU/changelogs/36200.txt b/fastlane/metadata/android/ru-RU/changelogs/36200.txt deleted file mode 100644 index 7d7bb34d..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/36200.txt +++ /dev/null @@ -1,5 +0,0 @@ -Что нового: -- Добавлено экстренное отключение VPN с обходом локальной сети -- Улучшенная скорость и надёжность автоматического туннелирования -- Улучшенная синхронизация плиток -- Различные исправления ошибок и улучшения diff --git a/fastlane/metadata/android/ru-RU/changelogs/36300.txt b/fastlane/metadata/android/ru-RU/changelogs/36300.txt deleted file mode 100644 index e33bbf75..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/36300.txt +++ /dev/null @@ -1,3 +0,0 @@ -Что нового: -- Исправлена ошибка переключения режима ядра -- Исправлен сбой, возникавший при уведомлении diff --git a/fastlane/metadata/android/ru-RU/changelogs/36500.txt b/fastlane/metadata/android/ru-RU/changelogs/36500.txt deleted file mode 100644 index 9a25064a..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/36500.txt +++ /dev/null @@ -1,6 +0,0 @@ -Что нового: -- Повышение надёжности автоматического туннелирования -- Исправлена ошибка сохранения разделенного туннеля и улучшена производительность -- Исправлена ошибка в режиме ядра -- Исправлена ошибка с плиткой -- Различные другие исправления и улучшения diff --git a/fastlane/metadata/android/ru-RU/changelogs/36600.txt b/fastlane/metadata/android/ru-RU/changelogs/36600.txt deleted file mode 100644 index b0fde170..00000000 --- a/fastlane/metadata/android/ru-RU/changelogs/36600.txt +++ /dev/null @@ -1,6 +0,0 @@ -Что нового: -- Функция пинга теперь работает независимо от автоматического туннелирования -- Функционал совместимости с Amnezia стал удобнее -- Добавлен функционал исключения локальной сети из туннеля -- Добавлен параметр задержки отбоя для автоматического туннелирования -- Множество исправлений ошибок и улучшений diff --git a/fastlane/metadata/android/ta-IN/changelogs/32100.txt b/fastlane/metadata/android/ta-IN/changelogs/32100.txt deleted file mode 100644 index 21f037c3..00000000 --- a/fastlane/metadata/android/ta-IN/changelogs/32100.txt +++ /dev/null @@ -1,3 +0,0 @@ -மேம்பாடுகள்: - - - sourceDir.walkTopDown().forEach { file -> - val zipFileName = file.absolutePath.removePrefix(sourceDir.absolutePath).removePrefix("/") - val entry = ZipEntry("$zipFileName${if (file.isDirectory) "/" else ""}") - zos.putNextEntry(entry) - if (file.isFile) { - file.inputStream().use { it.copyTo(zos) } - } - } - } - rotateIfNeeded() - } + suspend fun zipLogs(zipFilePath: String) = + withContext(ioDispatcher) { + outputStream?.close() + val sourceDir = File(logDir) + if (!sourceDir.exists() || !sourceDir.isDirectory) return@withContext + val outputZipFile = File(zipFilePath) + ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZipFile))).use { zos -> + sourceDir.walkTopDown().forEach { file -> + val zipFileName = + file.absolutePath.removePrefix(sourceDir.absolutePath).removePrefix("/") + val entry = ZipEntry("$zipFileName${if (file.isDirectory) "/" else ""}") + zos.putNextEntry(entry) + if (file.isFile) { + file.inputStream().use { it.copyTo(zos) } + } + } + } + rotateIfNeeded() + } - suspend fun deleteAllLogs() = withContext(ioDispatcher) { - outputStream?.close() - File(logDir).listFiles()?.forEach { it.delete() } - rotateIfNeeded() - } + suspend fun deleteAllLogs() = + withContext(ioDispatcher) { + outputStream?.close() + File(logDir).listFiles()?.forEach { it.delete() } + rotateIfNeeded() + } - fun close() { - outputStream?.close() - outputStream = null - currentFile = null - } + fun close() { + outputStream?.close() + outputStream = null + currentFile = null + } - private fun rotateIfNeeded() { - val folderSize = getFolderSize(File(logDir)) - if (folderSize >= maxFolderSize) { - deleteOldestFile() - } - val fileSize = currentFile?.length() ?: 0L - if (currentFile == null || fileSize >= maxFileSize) { - outputStream?.close() - currentFile = File(logDir, "logcat_${System.currentTimeMillis()}.txt") - outputStream = FileOutputStream(currentFile!!) - } - } + private fun rotateIfNeeded() { + val folderSize = getFolderSize(File(logDir)) + if (folderSize >= maxFolderSize) { + deleteOldestFile() + } + val fileSize = currentFile?.length() ?: 0L + if (currentFile == null || fileSize >= maxFileSize) { + outputStream?.close() + currentFile = File(logDir, "logcat_${System.currentTimeMillis()}.txt") + outputStream = FileOutputStream(currentFile!!) + } + } - private fun getFolderSize(dir: File): Long { - var size = 0L - if (dir.isDirectory && dir.listFiles() != null) { - dir.listFiles()!!.forEach { file -> - size += if (file.isDirectory) getFolderSize(file) else file.length() - } - } - return size - } + private fun getFolderSize(dir: File): Long { + var size = 0L + if (dir.isDirectory && dir.listFiles() != null) { + dir.listFiles()!!.forEach { file -> + size += if (file.isDirectory) getFolderSize(file) else file.length() + } + } + return size + } - private fun deleteOldestFile() { - File(logDir).listFiles() - ?.toList() - ?.minByOrNull { it.lastModified() } - ?.delete() - } + private fun deleteOldestFile() { + File(logDir).listFiles()?.toList()?.minByOrNull { it.lastModified() }?.delete() + } } diff --git a/logcatter/src/main/java/com/zaneschepke/logcatter/LogReader.kt b/logcatter/src/main/java/com/zaneschepke/logcatter/LogReader.kt index 8e908aff..22a2252e 100644 --- a/logcatter/src/main/java/com/zaneschepke/logcatter/LogReader.kt +++ b/logcatter/src/main/java/com/zaneschepke/logcatter/LogReader.kt @@ -4,10 +4,14 @@ import com.zaneschepke.logcatter.model.LogMessage import kotlinx.coroutines.flow.Flow interface LogReader { - fun start() - fun stop() - fun zipLogFiles(path: String) - suspend fun deleteAndClearLogs() - val bufferedLogs: Flow - val liveLogs: Flow + fun start() + + fun stop() + + fun zipLogFiles(path: String) + + suspend fun deleteAndClearLogs() + + val bufferedLogs: Flow + val liveLogs: Flow } diff --git a/logcatter/src/main/java/com/zaneschepke/logcatter/LogcatManager.kt b/logcatter/src/main/java/com/zaneschepke/logcatter/LogcatManager.kt index 8fe666ae..9978c4aa 100644 --- a/logcatter/src/main/java/com/zaneschepke/logcatter/LogcatManager.kt +++ b/logcatter/src/main/java/com/zaneschepke/logcatter/LogcatManager.kt @@ -15,78 +15,74 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch -class LogcatManager( - pid: Int, - logDir: String, - maxFileSize: Long, - maxFolderSize: Long, -) : LogReader, DefaultLifecycleObserver { - private val logScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - private val fileManager = LogFileManager(logDir, maxFileSize, maxFolderSize) - private val logcatReader = LogcatStreamReader(pid, fileManager) - private var logJob: Job? = null - private var isStarted = false +class LogcatManager(pid: Int, logDir: String, maxFileSize: Long, maxFolderSize: Long) : + LogReader, DefaultLifecycleObserver { + private val logScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private val fileManager = LogFileManager(logDir, maxFileSize, maxFolderSize) + private val logcatReader = LogcatStreamReader(pid, fileManager) + private var logJob: Job? = null + private var isStarted = false - private val _bufferedLogs = MutableSharedFlow( - replay = 10_000, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) - private val _liveLogs = MutableSharedFlow( - replay = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) + private val _bufferedLogs = + MutableSharedFlow( + replay = 10_000, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + private val _liveLogs = + MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) - override val bufferedLogs: Flow = _bufferedLogs.asSharedFlow() - override val liveLogs: Flow = _liveLogs.asSharedFlow() + override val bufferedLogs: Flow = _bufferedLogs.asSharedFlow() + override val liveLogs: Flow = _liveLogs.asSharedFlow() - override fun onCreate(owner: LifecycleOwner) { - // for auto start - // start() - } + override fun onCreate(owner: LifecycleOwner) { + // for auto start + // start() + } - override fun onDestroy(owner: LifecycleOwner) { - stop() - logScope.cancel() - } + override fun onDestroy(owner: LifecycleOwner) { + stop() + logScope.cancel() + } - override fun start() { - if (isStarted) return - stop() - logJob = logScope.launch { - logcatReader.readLogs().collect { logMessage -> - _bufferedLogs.emit(logMessage) - _liveLogs.emit(logMessage) - } - } - isStarted = true - } + override fun start() { + if (isStarted) return + stop() + logJob = + logScope.launch { + logcatReader.readLogs().collect { logMessage -> + _bufferedLogs.emit(logMessage) + _liveLogs.emit(logMessage) + } + } + isStarted = true + } - override fun stop() { - if (!isStarted) return - logJob?.cancel() - logcatReader.stop() - fileManager.close() - isStarted = false - } + override fun stop() { + if (!isStarted) return + logJob?.cancel() + logcatReader.stop() + fileManager.close() + isStarted = false + } - override fun zipLogFiles(path: String) { - logScope.launch { - val wasStarted = isStarted - stop() - fileManager.zipLogs(path) - if (wasStarted) { - logcatReader.clearLogs() - start() - } - } - } + override fun zipLogFiles(path: String) { + logScope.launch { + val wasStarted = isStarted + stop() + fileManager.zipLogs(path) + if (wasStarted) { + logcatReader.clearLogs() + start() + } + } + } - @OptIn(ExperimentalCoroutinesApi::class) - override suspend fun deleteAndClearLogs() { - val wasStarted = isStarted - stop() - _bufferedLogs.resetReplayCache() - fileManager.deleteAllLogs() - if (wasStarted) start() - } + @OptIn(ExperimentalCoroutinesApi::class) + override suspend fun deleteAndClearLogs() { + val wasStarted = isStarted + stop() + _bufferedLogs.resetReplayCache() + fileManager.deleteAllLogs() + if (wasStarted) start() + } } diff --git a/logcatter/src/main/java/com/zaneschepke/logcatter/LogcatReader.kt b/logcatter/src/main/java/com/zaneschepke/logcatter/LogcatReader.kt index 31b8fb7b..00614523 100644 --- a/logcatter/src/main/java/com/zaneschepke/logcatter/LogcatReader.kt +++ b/logcatter/src/main/java/com/zaneschepke/logcatter/LogcatReader.kt @@ -4,29 +4,34 @@ import androidx.lifecycle.ProcessLifecycleOwner import java.io.File object LogcatReader { - private const val MAX_FILE_SIZE = 2097152L // 2MB - private const val MAX_FOLDER_SIZE = 10485760L // 10MB + private const val MAX_FILE_SIZE = 2097152L // 2MB + private const val MAX_FOLDER_SIZE = 10485760L // 10MB - private lateinit var logcatManager: LogcatManager - private var isInitialized = false + private lateinit var logcatManager: LogcatManager + private var isInitialized = false - fun init(maxFileSize: Long = MAX_FILE_SIZE, maxFolderSize: Long = MAX_FOLDER_SIZE, storageDir: String): LogReader { - if (maxFileSize > maxFolderSize) { - throw IllegalStateException("maxFileSize must be less than maxFolderSize") - } - synchronized(this) { - if (isInitialized) return logcatManager - val logDir = "$storageDir${File.separator}logs" - File(logDir).mkdirs() - logcatManager = LogcatManager( - pid = android.os.Process.myPid(), - logDir = logDir, - maxFileSize = maxFileSize, - maxFolderSize = maxFolderSize, - ) - ProcessLifecycleOwner.get().lifecycle.addObserver(logcatManager) - isInitialized = true - return logcatManager - } - } + fun init( + maxFileSize: Long = MAX_FILE_SIZE, + maxFolderSize: Long = MAX_FOLDER_SIZE, + storageDir: String, + ): LogReader { + if (maxFileSize > maxFolderSize) { + throw IllegalStateException("maxFileSize must be less than maxFolderSize") + } + synchronized(this) { + if (isInitialized) return logcatManager + val logDir = "$storageDir${File.separator}logs" + File(logDir).mkdirs() + logcatManager = + LogcatManager( + pid = android.os.Process.myPid(), + logDir = logDir, + maxFileSize = maxFileSize, + maxFolderSize = maxFolderSize, + ) + ProcessLifecycleOwner.get().lifecycle.addObserver(logcatManager) + isInitialized = true + return logcatManager + } + } } diff --git a/logcatter/src/main/java/com/zaneschepke/logcatter/LogcatStreamReader.kt b/logcatter/src/main/java/com/zaneschepke/logcatter/LogcatStreamReader.kt index 8297b835..9324376f 100644 --- a/logcatter/src/main/java/com/zaneschepke/logcatter/LogcatStreamReader.kt +++ b/logcatter/src/main/java/com/zaneschepke/logcatter/LogcatStreamReader.kt @@ -1,63 +1,62 @@ package com.zaneschepke.logcatter import com.zaneschepke.logcatter.model.LogMessage +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStreamReader import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn -import java.io.BufferedReader -import java.io.IOException -import java.io.InputStreamReader -class LogcatStreamReader( - private val pid: Int, - private val fileManager: LogFileManager, -) { - private val bufferSize = 1024 - private var process: Process? = null - private var reader: BufferedReader? = null - private val command = "logcat -v epoch | grep \"($pid)\"" - private val clearCommand = "logcat -c" +class LogcatStreamReader(private val pid: Int, private val fileManager: LogFileManager) { + private val bufferSize = 1024 + private var process: Process? = null + private var reader: BufferedReader? = null + private val command = "logcat -v epoch | grep \"($pid)\"" + private val clearCommand = "logcat -c" - private val ioDispatcher = Dispatchers.IO + private val ioDispatcher = Dispatchers.IO - fun readLogs(): Flow = flow { - try { - clearLogs() - process = Runtime.getRuntime().exec(command) - reader = BufferedReader(InputStreamReader(process!!.inputStream), bufferSize) - reader!!.lineSequence().forEach { line -> - if (line.isNotEmpty()) { - fileManager.writeLog(line) - emit(LogMessage.from(line)) - } - } - } catch (e: IOException) { - // do nothing - } finally { - stop() - } - }.flowOn(ioDispatcher) + fun readLogs(): Flow = + flow { + try { + clearLogs() + process = Runtime.getRuntime().exec(command) + reader = BufferedReader(InputStreamReader(process!!.inputStream), bufferSize) + reader!!.lineSequence().forEach { line -> + if (line.isNotEmpty()) { + fileManager.writeLog(line) + emit(LogMessage.from(line)) + } + } + } catch (e: IOException) { + // do nothing + } finally { + stop() + } + } + .flowOn(ioDispatcher) - fun start() { - if (process == null) { - try { - process = Runtime.getRuntime().exec(command) - reader = BufferedReader(InputStreamReader(process!!.inputStream), bufferSize) - } catch (e: IOException) { - // do nothing - } - } - } + fun start() { + if (process == null) { + try { + process = Runtime.getRuntime().exec(command) + reader = BufferedReader(InputStreamReader(process!!.inputStream), bufferSize) + } catch (e: IOException) { + // do nothing + } + } + } - fun stop() { - process?.destroy() - reader?.close() - process = null - reader = null - } + fun stop() { + process?.destroy() + reader?.close() + process = null + reader = null + } - fun clearLogs() { - Runtime.getRuntime().exec(clearCommand) - } + fun clearLogs() { + Runtime.getRuntime().exec(clearCommand) + } } diff --git a/logcatter/src/main/java/com/zaneschepke/logcatter/model/LogLevel.kt b/logcatter/src/main/java/com/zaneschepke/logcatter/model/LogLevel.kt index 32befcab..e427db5e 100644 --- a/logcatter/src/main/java/com/zaneschepke/logcatter/model/LogLevel.kt +++ b/logcatter/src/main/java/com/zaneschepke/logcatter/model/LogLevel.kt @@ -1,50 +1,50 @@ package com.zaneschepke.logcatter.model enum class LogLevel(val signifier: String) { - DEBUG("D") { - override fun color(): Long { - return 0xFF2196F3 - } - }, - INFO("I") { - override fun color(): Long { - return 0xFF4CAF50 - } - }, - ASSERT("A") { - override fun color(): Long { - return 0xFF9C27B0 - } - }, - WARNING("W") { - override fun color(): Long { - return 0xFFFFC107 - } - }, - ERROR("E") { - override fun color(): Long { - return 0xFFF44336 - } - }, - VERBOSE("V") { - override fun color(): Long { - return 0xFF000000 - } - }, ; + DEBUG("D") { + override fun color(): Long { + return 0xFF2196F3 + } + }, + INFO("I") { + override fun color(): Long { + return 0xFF4CAF50 + } + }, + ASSERT("A") { + override fun color(): Long { + return 0xFF9C27B0 + } + }, + WARNING("W") { + override fun color(): Long { + return 0xFFFFC107 + } + }, + ERROR("E") { + override fun color(): Long { + return 0xFFF44336 + } + }, + VERBOSE("V") { + override fun color(): Long { + return 0xFF000000 + } + }; - abstract fun color(): Long + abstract fun color(): Long - companion object { - fun fromSignifier(signifier: String): LogLevel { - return when (signifier) { - DEBUG.signifier -> DEBUG - INFO.signifier -> INFO - WARNING.signifier -> WARNING - VERBOSE.signifier -> VERBOSE - ASSERT.signifier -> ASSERT - ERROR.signifier -> ERROR - else -> VERBOSE - } - } - } + companion object { + fun fromSignifier(signifier: String): LogLevel { + return when (signifier) { + DEBUG.signifier -> DEBUG + INFO.signifier -> INFO + WARNING.signifier -> WARNING + VERBOSE.signifier -> VERBOSE + ASSERT.signifier -> ASSERT + ERROR.signifier -> ERROR + else -> VERBOSE + } + } + } } diff --git a/logcatter/src/main/java/com/zaneschepke/logcatter/model/LogMessage.kt b/logcatter/src/main/java/com/zaneschepke/logcatter/model/LogMessage.kt index 46bbf6d9..5b390189 100644 --- a/logcatter/src/main/java/com/zaneschepke/logcatter/model/LogMessage.kt +++ b/logcatter/src/main/java/com/zaneschepke/logcatter/model/LogMessage.kt @@ -3,42 +3,42 @@ package com.zaneschepke.logcatter.model import java.time.Instant data class LogMessage( - val time: String, - val pid: String, - val tid: String, - val level: LogLevel, - val tag: String, - val message: String, + val time: String, + val pid: String, + val tid: String, + val level: LogLevel, + val tag: String, + val message: String, ) { - override fun toString(): String { - return "$time $pid $tid $level $tag message= $message" - } + override fun toString(): String { + return "$time $pid $tid $level $tag message= $message" + } - companion object { - fun from(logcatLine: String): LogMessage { - return if (logcatLine.contains("---------")) { - LogMessage( - Instant.now().toString(), - "0", - "0", - LogLevel.VERBOSE, - "System", - logcatLine, - ) - } else { - // TODO improve this - val parts = logcatLine.trim().split(" ").filter { it.isNotEmpty() } - val epochParts = parts[0].split(".").map { it.toLong() } - val message = parts.subList(5, parts.size).joinToString(" ") - LogMessage( - Instant.ofEpochSecond(epochParts[0], epochParts[1]).toString(), - parts[1], - parts[2], - LogLevel.fromSignifier(parts[3]), - parts[4], - message, - ) - } - } - } + companion object { + fun from(logcatLine: String): LogMessage { + return if (logcatLine.contains("---------")) { + LogMessage( + Instant.now().toString(), + "0", + "0", + LogLevel.VERBOSE, + "System", + logcatLine, + ) + } else { + // TODO improve this + val parts = logcatLine.trim().split(" ").filter { it.isNotEmpty() } + val epochParts = parts[0].split(".").map { it.toLong() } + val message = parts.subList(5, parts.size).joinToString(" ") + LogMessage( + Instant.ofEpochSecond(epochParts[0], epochParts[1]).toString(), + parts[1], + parts[2], + LogLevel.fromSignifier(parts[3]), + parts[4], + message, + ) + } + } + } } diff --git a/logcatter/src/test/java/com/zaneschepke/ExampleUnitTest.kt b/logcatter/src/test/java/com/zaneschepke/ExampleUnitTest.kt index 67c1dfb7..903247b6 100644 --- a/logcatter/src/test/java/com/zaneschepke/ExampleUnitTest.kt +++ b/logcatter/src/test/java/com/zaneschepke/ExampleUnitTest.kt @@ -9,8 +9,8 @@ import org.junit.Test * See [testing documentation](http://d.android.com/tools/testing). */ class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } } diff --git a/networkmonitor/build.gradle.kts b/networkmonitor/build.gradle.kts index 1b9ffd4b..7f1ec442 100644 --- a/networkmonitor/build.gradle.kts +++ b/networkmonitor/build.gradle.kts @@ -1,51 +1,47 @@ plugins { - alias(libs.plugins.androidLibrary) - alias(libs.plugins.kotlin.android) + alias(libs.plugins.androidLibrary) + alias(libs.plugins.kotlin.android) } android { - namespace = "com.zaneschepke.networkmonitor" - compileSdk = 34 + namespace = "com.zaneschepke.networkmonitor" + compileSdk = 34 - defaultConfig { - minSdk = 26 + defaultConfig { + minSdk = 26 - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } - buildTypes { - release { - isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") - } - create(Constants.PRERELEASE) { - initWith(getByName(Constants.RELEASE)) - } + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + create(Constants.PRERELEASE) { initWith(getByName(Constants.RELEASE)) } - create(Constants.NIGHTLY) { - initWith(getByName(Constants.RELEASE)) - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - kotlinOptions { - jvmTarget = Constants.JVM_TARGET - } + create(Constants.NIGHTLY) { initWith(getByName(Constants.RELEASE)) } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { jvmTarget = Constants.JVM_TARGET } } dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.appcompat) - implementation(libs.material) - testImplementation(libs.junit) - androidTestImplementation(libs.androidx.junit) - androidTestImplementation(libs.androidx.espresso.core) + implementation(libs.tunnel) - implementation(libs.tunnel) - - implementation(libs.timber) + implementation(libs.timber) } diff --git a/networkmonitor/src/androidTest/java/com/zaneschepke/networkmonitor/ExampleInstrumentedTest.kt b/networkmonitor/src/androidTest/java/com/zaneschepke/networkmonitor/ExampleInstrumentedTest.kt index 2cf6dc97..50996862 100644 --- a/networkmonitor/src/androidTest/java/com/zaneschepke/networkmonitor/ExampleInstrumentedTest.kt +++ b/networkmonitor/src/androidTest/java/com/zaneschepke/networkmonitor/ExampleInstrumentedTest.kt @@ -13,10 +13,10 @@ import org.junit.runner.RunWith */ @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.zaneschepke.networkmonitor.test", appContext.packageName) - } + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.zaneschepke.networkmonitor.test", appContext.packageName) + } } diff --git a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/AndroidNetworkMonitor.kt b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/AndroidNetworkMonitor.kt index 91030f36..1d9913c7 100644 --- a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/AndroidNetworkMonitor.kt +++ b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/AndroidNetworkMonitor.kt @@ -21,201 +21,219 @@ import kotlinx.coroutines.runBlocking import timber.log.Timber class AndroidNetworkMonitor( - context: Context, - private val useRootShellCallback: suspend () -> Boolean, + context: Context, + private val useRootShellCallback: suspend () -> Boolean, ) : NetworkMonitor { - companion object { - const val LOCATION_GRANTED = "LOCATION_PERMISSIONS_GRANTED" - const val LOCATION_SERVICES_FILTER = "android.location.PROVIDERS_CHANGED" - } + companion object { + const val LOCATION_GRANTED = "LOCATION_PERMISSIONS_GRANTED" + const val LOCATION_SERVICES_FILTER = "android.location.PROVIDERS_CHANGED" + } - private val appContext = context.applicationContext - private val packageName = appContext.packageName - private val connectivityManager = appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - private val wifiManager = appContext.getSystemService(Context.WIFI_SERVICE) as WifiManager? - private val locationManager = appContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager - private val rootShell = RootShell(context) + private val appContext = context.applicationContext + private val packageName = appContext.packageName + private val connectivityManager = + appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + private val wifiManager = appContext.getSystemService(Context.WIFI_SERVICE) as WifiManager? + private val locationManager = + appContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager + private val rootShell = RootShell(context) - @get:Synchronized @set:Synchronized - var currentSsid: String? = null + @get:Synchronized @set:Synchronized var currentSsid: String? = null - @get:Synchronized @set:Synchronized - var wifiConnected = false + @get:Synchronized @set:Synchronized var wifiConnected = false - data class WifiState(val connected: Boolean = false, val ssid: String? = null) - data class TransportState(val connected: Boolean = false) + data class WifiState(val connected: Boolean = false, val ssid: String? = null) - private val wifiFlow: Flow = callbackFlow { + data class TransportState(val connected: Boolean = false) - @Suppress("DEPRECATION") - fun getWifiSsid(): String? { - return if (runBlocking { useRootShellCallback() }) { - rootShell.getCurrentWifiName() - } else { - if (wifiManager == null) return null - try { - wifiManager.connectionInfo?.ssid?.trim('"')?.takeIf { it.isNotEmpty() } - } catch (e: Exception) { - Timber.e(e) - null - } - } - } + private val wifiFlow: Flow = callbackFlow { + @Suppress("DEPRECATION") + fun getWifiSsid(): String? { + return if (runBlocking { useRootShellCallback() }) { + rootShell.getCurrentWifiName() + } else { + if (wifiManager == null) return null + try { + wifiManager.connectionInfo?.ssid?.trim('"')?.takeIf { it.isNotEmpty() } + } catch (e: Exception) { + Timber.e(e) + null + } + } + } - fun handleUnknownWifi() { - val newSsid = getWifiSsid() - // Only update if new SSID is valid; preserve existing valid SSID otherwise - if (newSsid != null && newSsid != WifiManager.UNKNOWN_SSID) { - currentSsid = newSsid - trySend(WifiState(connected = wifiConnected, ssid = currentSsid)) - } else if (currentSsid == null || currentSsid == WifiManager.UNKNOWN_SSID) { - currentSsid = newSsid - trySend(WifiState(connected = wifiConnected, ssid = currentSsid)) - } - Timber.d("handleUnknownWifi: currentSsid=$currentSsid") - } + fun handleUnknownWifi() { + val newSsid = getWifiSsid() + // Only update if new SSID is valid; preserve existing valid SSID otherwise + if (newSsid != null && newSsid != WifiManager.UNKNOWN_SSID) { + currentSsid = newSsid + trySend(WifiState(connected = wifiConnected, ssid = currentSsid)) + } else if (currentSsid == null || currentSsid == WifiManager.UNKNOWN_SSID) { + currentSsid = newSsid + trySend(WifiState(connected = wifiConnected, ssid = currentSsid)) + } + Timber.d("handleUnknownWifi: currentSsid=$currentSsid") + } - val locationPermissionReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - Timber.d("locationPermissionReceiver received intent with action: ${intent.action}") - if (intent.action == "$packageName.$LOCATION_GRANTED") { - Timber.d("Received update: Precise and all-the-time location permissions are enabled") - handleUnknownWifi() - } - } - } + val locationPermissionReceiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + Timber.d( + "locationPermissionReceiver received intent with action: ${intent.action}" + ) + if (intent.action == "$packageName.$LOCATION_GRANTED") { + Timber.d( + "Received update: Precise and all-the-time location permissions are enabled" + ) + handleUnknownWifi() + } + } + } - val locationServicesReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (intent.action == LOCATION_SERVICES_FILTER) { - val isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) - val isNetworkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) - val isLocationServicesEnabled = isGpsEnabled || isNetworkEnabled - Timber.d("Location Services state changed. Enabled: $isLocationServicesEnabled, GPS: $isGpsEnabled, Network: $isNetworkEnabled") - if (isLocationServicesEnabled) handleUnknownWifi() - } - } - } + val locationServicesReceiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == LOCATION_SERVICES_FILTER) { + val isGpsEnabled = + locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) + val isNetworkEnabled = + locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) + val isLocationServicesEnabled = isGpsEnabled || isNetworkEnabled + Timber.d( + "Location Services state changed. Enabled: $isLocationServicesEnabled, GPS: $isGpsEnabled, Network: $isNetworkEnabled" + ) + if (isLocationServicesEnabled) handleUnknownWifi() + } + } + } - // Use RECEIVER_NOT_EXPORTED for Android 14+ compatibility - val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - Context.RECEIVER_EXPORTED - } else { - 0 - } + // Use RECEIVER_NOT_EXPORTED for Android 14+ compatibility + val flags = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + Context.RECEIVER_EXPORTED + } else { + 0 + } - appContext.registerReceiver( - locationPermissionReceiver, - IntentFilter("$packageName.$LOCATION_GRANTED"), - flags, - ) + appContext.registerReceiver( + locationPermissionReceiver, + IntentFilter("$packageName.$LOCATION_GRANTED"), + flags, + ) - appContext.registerReceiver( - locationServicesReceiver, - IntentFilter(LOCATION_SERVICES_FILTER), - flags, - ) + appContext.registerReceiver( + locationServicesReceiver, + IntentFilter(LOCATION_SERVICES_FILTER), + flags, + ) - val callback = object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - Timber.d("Wi-Fi onAvailable: network=$network") - currentSsid = getWifiSsid() - wifiConnected = true - trySend(WifiState(connected = true, ssid = currentSsid)) - } + val callback = + object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + Timber.d("Wi-Fi onAvailable: network=$network") + currentSsid = getWifiSsid() + wifiConnected = true + trySend(WifiState(connected = true, ssid = currentSsid)) + } - override fun onLost(network: Network) { - Timber.d("Wi-Fi onLost: network=$network") - currentSsid = null - wifiConnected = false - trySend(WifiState(connected = false, ssid = null)) - } - } + override fun onLost(network: Network) { + Timber.d("Wi-Fi onLost: network=$network") + currentSsid = null + wifiConnected = false + trySend(WifiState(connected = false, ssid = null)) + } + } - val request = NetworkRequest.Builder() - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) - .build() + val request = + NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .build() - connectivityManager.registerNetworkCallback(request, callback) - trySend(WifiState()) + connectivityManager.registerNetworkCallback(request, callback) + trySend(WifiState()) - awaitClose { - connectivityManager.unregisterNetworkCallback(callback) - appContext.unregisterReceiver(locationPermissionReceiver) - appContext.unregisterReceiver(locationServicesReceiver) - } - } + awaitClose { + connectivityManager.unregisterNetworkCallback(callback) + appContext.unregisterReceiver(locationPermissionReceiver) + appContext.unregisterReceiver(locationServicesReceiver) + } + } - private val cellularFlow: Flow = callbackFlow { - val callback = object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - Timber.d("Cellular onAvailable: network=$network") - trySend(TransportState(connected = true)) - } + private val cellularFlow: Flow = callbackFlow { + val callback = + object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + Timber.d("Cellular onAvailable: network=$network") + trySend(TransportState(connected = true)) + } - override fun onLost(network: Network) { - Timber.d("Cellular onLost: network=$network") - trySend(TransportState(connected = false)) - } - } + override fun onLost(network: Network) { + Timber.d("Cellular onLost: network=$network") + trySend(TransportState(connected = false)) + } + } - val request = NetworkRequest.Builder() - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) - .build() + val request = + NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .build() - connectivityManager.registerNetworkCallback(request, callback) - trySend(TransportState()) + connectivityManager.registerNetworkCallback(request, callback) + trySend(TransportState()) - awaitClose { - connectivityManager.unregisterNetworkCallback(callback) - } - } + awaitClose { connectivityManager.unregisterNetworkCallback(callback) } + } - private val ethernetFlow: Flow = callbackFlow { - val callback = object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - Timber.d("Ethernet onAvailable: network=$network") - trySend(TransportState(connected = true)) - } + private val ethernetFlow: Flow = callbackFlow { + val callback = + object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + Timber.d("Ethernet onAvailable: network=$network") + trySend(TransportState(connected = true)) + } - override fun onLost(network: Network) { - Timber.d("Ethernet onLost: network=$network") - trySend(TransportState(connected = false)) - } - } + override fun onLost(network: Network) { + Timber.d("Ethernet onLost: network=$network") + trySend(TransportState(connected = false)) + } + } - val request = NetworkRequest.Builder() - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) - .build() + val request = + NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) + .build() - connectivityManager.registerNetworkCallback(request, callback) - trySend(TransportState()) + connectivityManager.registerNetworkCallback(request, callback) + trySend(TransportState()) - awaitClose { connectivityManager.unregisterNetworkCallback(callback) } - } + awaitClose { connectivityManager.unregisterNetworkCallback(callback) } + } - override val networkStatusFlow = combine(wifiFlow, cellularFlow, ethernetFlow) { wifi, cellular, ethernet -> - val hasAnyConnection = wifi.connected || cellular.connected || ethernet.connected - if (hasAnyConnection) { - NetworkStatus.Connected( - wifiSsid = wifi.ssid, - wifiConnected = wifi.connected, - cellularConnected = cellular.connected, - ethernetConnected = ethernet.connected, - ) - } else { - NetworkStatus.Disconnected - }.also { Timber.d("NetworkStatus: $it") } - }.distinctUntilChanged() + override val networkStatusFlow = + combine(wifiFlow, cellularFlow, ethernetFlow) { wifi, cellular, ethernet -> + val hasAnyConnection = wifi.connected || cellular.connected || ethernet.connected + if (hasAnyConnection) { + NetworkStatus.Connected( + wifiSsid = wifi.ssid, + wifiConnected = wifi.connected, + cellularConnected = cellular.connected, + ethernetConnected = ethernet.connected, + ) + } else { + NetworkStatus.Disconnected + } + .also { Timber.d("NetworkStatus: $it") } + } + .distinctUntilChanged() - override fun sendLocationPermissionsGrantedBroadcast() { - val action = "$packageName.$LOCATION_GRANTED" - val intent = Intent(action) - Timber.d("Sending broadcast: $action") - appContext.sendBroadcast(intent) - } + override fun sendLocationPermissionsGrantedBroadcast() { + val action = "$packageName.$LOCATION_GRANTED" + val intent = Intent(action) + Timber.d("Sending broadcast: $action") + appContext.sendBroadcast(intent) + } } diff --git a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/Extensions.kt b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/Extensions.kt index c735dce0..d93445c9 100644 --- a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/Extensions.kt +++ b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/Extensions.kt @@ -3,7 +3,10 @@ package com.zaneschepke.networkmonitor import com.wireguard.android.util.RootShell fun RootShell.getCurrentWifiName(): String? { - val response = mutableListOf() - this.run(response, "dumpsys wifi | grep 'Supplicant state: COMPLETED' | grep -o 'SSID: [^,]*' | cut -d ' ' -f2- | tr -d '\"'") - return response.firstOrNull() + val response = mutableListOf() + this.run( + response, + "dumpsys wifi | grep 'Supplicant state: COMPLETED' | grep -o 'SSID: [^,]*' | cut -d ' ' -f2- | tr -d '\"'", + ) + return response.firstOrNull() } diff --git a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/NetworkMonitor.kt b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/NetworkMonitor.kt index 098113e0..84f4eda8 100644 --- a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/NetworkMonitor.kt +++ b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/NetworkMonitor.kt @@ -3,6 +3,7 @@ package com.zaneschepke.networkmonitor import kotlinx.coroutines.flow.Flow interface NetworkMonitor { - val networkStatusFlow: Flow - fun sendLocationPermissionsGrantedBroadcast() + val networkStatusFlow: Flow + + fun sendLocationPermissionsGrantedBroadcast() } diff --git a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/NetworkStatus.kt b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/NetworkStatus.kt index 3d03613b..fa7ab27c 100644 --- a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/NetworkStatus.kt +++ b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/NetworkStatus.kt @@ -1,20 +1,20 @@ package com.zaneschepke.networkmonitor sealed class NetworkStatus { - data object Disconnected : NetworkStatus() { - override val wifiConnected = false - override val ethernetConnected = false - override val cellularConnected = false - } + data object Disconnected : NetworkStatus() { + override val wifiConnected = false + override val ethernetConnected = false + override val cellularConnected = false + } - data class Connected( - val wifiSsid: String? = null, - override val wifiConnected: Boolean = false, - override val ethernetConnected: Boolean = false, - override val cellularConnected: Boolean = false, - ) : NetworkStatus() + data class Connected( + val wifiSsid: String? = null, + override val wifiConnected: Boolean = false, + override val ethernetConnected: Boolean = false, + override val cellularConnected: Boolean = false, + ) : NetworkStatus() - abstract val wifiConnected: Boolean - abstract val ethernetConnected: Boolean - abstract val cellularConnected: Boolean + abstract val wifiConnected: Boolean + abstract val ethernetConnected: Boolean + abstract val cellularConnected: Boolean } diff --git a/networkmonitor/src/test/java/com/zaneschepke/networkmonitor/ExampleUnitTest.kt b/networkmonitor/src/test/java/com/zaneschepke/networkmonitor/ExampleUnitTest.kt index 6ea5cebb..7cdfbf84 100644 --- a/networkmonitor/src/test/java/com/zaneschepke/networkmonitor/ExampleUnitTest.kt +++ b/networkmonitor/src/test/java/com/zaneschepke/networkmonitor/ExampleUnitTest.kt @@ -9,8 +9,8 @@ import org.junit.Test * See [testing documentation](http://d.android.com/tools/testing). */ class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } } diff --git a/versionCode.txt b/versionCode.txt index c2270834..0cfbf088 100644 --- a/versionCode.txt +++ b/versionCode.txt @@ -1 +1 @@ -0 \ No newline at end of file +2