From 80a02382e1b278bbbe24d5afde23a66a2b26b8eb Mon Sep 17 00:00:00 2001 From: Zane Schepke Date: Sun, 28 Jul 2024 14:49:35 -0400 Subject: [PATCH] ci: add basic ci (#287) - add ktlint --- .editorconfig | 34 +- .github/workflows/ci.yml | 23 + app/build.gradle.kts | 308 ++-- .../ExampleInstrumentedTest.kt | 12 +- .../wireguardautotunnel/MigrationTest.kt | 52 +- .../WireGuardAutoTunnel.kt | 72 +- .../wireguardautotunnel/data/AppDatabase.kt | 64 +- .../data/DatabaseCallback.kt | 26 +- .../data/DatabaseListConverters.kt | 30 +- .../wireguardautotunnel/data/Queries.kt | 60 +- .../wireguardautotunnel/data/SettingsDao.kt | 32 +- .../data/TunnelConfigDao.kt | 52 +- .../data/datastore/DataStoreManager.kt | 106 +- .../data/domain/GeneralState.kt | 22 +- .../data/domain/Settings.kt | 98 +- .../data/domain/TunnelConfig.kt | 76 +- .../data/repository/AppDataRepository.kt | 13 +- .../data/repository/AppDataRoomRepository.kt | 54 +- .../data/repository/AppStateRepository.kt | 29 +- .../repository/DataStoreAppStateRepository.kt | 140 +- .../data/repository/RoomSettingsRepository.kt | 25 +- .../repository/RoomTunnelConfigRepository.kt | 103 +- .../data/repository/SettingsRepository.kt | 8 +- .../data/repository/TunnelConfigRepository.kt | 25 +- .../wireguardautotunnel/module/AppModule.kt | 22 +- .../module/CoroutinesDispatchersModule.kt | 24 +- .../module/RepositoryModule.kt | 107 +- .../module/ServiceModule.kt | 30 +- .../module/TunnelModule.kt | 95 +- .../module/ViewModelModule.kt | 14 +- .../receiver/BootReceiver.kt | 71 +- .../receiver/NotificationActionReceiver.kt | 42 +- .../service/foreground/Action.kt | 8 +- .../service/foreground/ForegroundService.kt | 79 +- .../service/foreground/ServiceManager.kt | 199 ++- .../service/foreground/WatcherState.kt | 103 +- .../WireGuardConnectivityWatcherService.kt | 770 +++++----- .../foreground/WireGuardTunnelService.kt | 301 ++-- .../service/network/BaseNetworkService.kt | 193 ++- .../service/network/EthernetService.kt | 8 +- .../service/network/MobileDataService.kt | 8 +- .../service/network/NetworkService.kt | 4 +- .../service/network/NetworkStatus.kt | 8 +- .../service/network/WifiService.kt | 8 +- .../notification/NotificationService.kt | 28 +- .../notification/WireGuardNotification.kt | 166 +- .../service/shortcut/ShortcutsActivity.kt | 114 +- .../service/tile/AutoTunnelControlTile.kt | 141 +- .../service/tile/TunnelControlTile.kt | 166 +- .../service/tunnel/HandshakeStatus.kt | 23 +- .../service/tunnel/TunnelState.kt | 67 +- .../service/tunnel/VpnService.kt | 8 +- .../service/tunnel/VpnState.kt | 6 +- .../service/tunnel/WireGuardTunnel.kt | 343 +++-- .../tunnel/statistics/AmneziaStatistics.kt | 46 +- .../tunnel/statistics/TunnelStatistics.kt | 14 +- .../tunnel/statistics/WireGuardStatistics.kt | 50 +- .../wireguardautotunnel/ui/AppUiState.kt | 10 +- .../wireguardautotunnel/ui/AppViewModel.kt | 180 +-- .../wireguardautotunnel/ui/MainActivity.kt | 418 ++--- .../wireguardautotunnel/ui/Screen.kt | 57 +- .../wireguardautotunnel/ui/SplashActivity.kt | 69 +- .../ui/common/ClickableIconButton.kt | 48 +- .../ui/common/RowListItem.kt | 135 +- .../ui/common/SearchBar.kt | 102 +- .../ui/common/config/ConfigurationTextBox.kt | 42 +- .../ui/common/config/ConfigurationToggle.kt | 58 +- .../ui/common/navigation/BottomNavBar.kt | 65 +- .../ui/common/navigation/BottomNavItem.kt | 6 +- .../ui/common/prompt/AuthorizationPrompt.kt | 127 +- .../ui/common/prompt/CustomSnackbar.kt | 69 +- .../ui/common/screen/LoadingScreen.kt | 21 +- .../ui/common/text/LogTypeLabel.kt | 19 +- .../ui/common/text/SectionTitle.kt | 12 +- .../ui/screens/config/ConfigScreen.kt | 1354 +++++++++-------- .../ui/screens/config/ConfigUiState.kt | 122 +- .../ui/screens/config/ConfigViewModel.kt | 910 +++++------ .../ui/screens/config/model/InterfaceProxy.kt | 175 ++- .../ui/screens/config/model/PeerProxy.kt | 178 +-- .../ui/screens/main/ConfigType.kt | 4 +- .../ui/screens/main/MainScreen.kt | 1289 ++++++++-------- .../ui/screens/main/MainUiState.kt | 8 +- .../ui/screens/main/MainViewModel.kt | 636 ++++---- .../ui/screens/options/OptionsScreen.kt | 558 +++---- .../ui/screens/options/OptionsUiState.kt | 6 +- .../ui/screens/options/OptionsViewModel.kt | 150 +- .../ui/screens/pinlock/PinLockScreen.kt | 74 +- .../ui/screens/settings/SettingsScreen.kt | 1206 ++++++++------- .../ui/screens/settings/SettingsUiState.kt | 12 +- .../ui/screens/settings/SettingsViewModel.kt | 383 +++-- .../ui/screens/support/SupportScreen.kt | 516 +++---- .../ui/screens/support/SupportViewModel.kt | 25 +- .../ui/screens/support/logs/LogsScreen.kt | 157 +- .../ui/screens/support/logs/LogsViewModel.kt | 59 +- .../wireguardautotunnel/ui/theme/Theme.kt | 90 +- .../wireguardautotunnel/ui/theme/Type.kt | 20 +- .../wireguardautotunnel/util/Constants.kt | 64 +- .../wireguardautotunnel/util/Extensions.kt | 156 +- .../wireguardautotunnel/util/FileUtils.kt | 206 ++- .../wireguardautotunnel/util/NumberUtils.kt | 52 +- .../wireguardautotunnel/util/ReleaseTree.kt | 18 +- .../wireguardautotunnel/util/StringValue.kt | 27 +- .../util/WgTunnelExceptions.kt | 249 +-- .../wireguardautotunnel/ExampleUnitTest.kt | 8 +- build.gradle.kts | 30 +- gradle/libs.versions.toml | 2 + logcatter/build.gradle.kts | 72 +- .../zaneschepke/ExampleInstrumentedTest.kt | 12 +- .../logcatter/LocalLogCollector.kt | 12 +- .../com/zaneschepke/logcatter/LogcatUtil.kt | 276 ++++ .../com/zaneschepke/logcatter/Logcatter.kt | 271 ---- .../zaneschepke/logcatter/model/LogLevel.kt | 88 +- .../zaneschepke/logcatter/model/LogMessage.kt | 72 +- .../java/com/zaneschepke/ExampleUnitTest.kt | 8 +- settings.gradle.kts | 44 +- 115 files changed, 8083 insertions(+), 7684 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 logcatter/src/main/java/com/zaneschepke/logcatter/LogcatUtil.kt delete mode 100644 logcatter/src/main/java/com/zaneschepke/logcatter/Logcatter.kt diff --git a/.editorconfig b/.editorconfig index de4a9d84..fcefbfeb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,8 +1,14 @@ -[{*.kt,*.kts}] -indent_style = space -insert_final_newline = true -max_line_length = 100 +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 @@ -11,8 +17,6 @@ ij_kotlin_align_multiline_extends_list = false ij_kotlin_align_multiline_method_parentheses = false ij_kotlin_align_multiline_parameters = true ij_kotlin_align_multiline_parameters_in_calls = false -ij_kotlin_allow_trailing_comma = true -ij_kotlin_allow_trailing_comma_on_call_site = true ij_kotlin_assignment_wrap = normal ij_kotlin_blank_lines_after_class_header = 0 ij_kotlin_blank_lines_around_block_when_branches = 0 @@ -20,10 +24,7 @@ ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_ ij_kotlin_block_comment_at_first_column = true ij_kotlin_call_parameters_new_line_after_left_paren = true ij_kotlin_call_parameters_right_paren_on_new_line = false -ij_kotlin_call_parameters_wrap = on_every_item ij_kotlin_catch_on_new_line = false -ij_kotlin_class_annotation_wrap = split_into_lines -ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL ij_kotlin_continuation_indent_for_chained_calls = true ij_kotlin_continuation_indent_for_expression_bodies = true ij_kotlin_continuation_indent_in_argument_lists = true @@ -52,7 +53,6 @@ ij_kotlin_method_annotation_wrap = split_into_lines ij_kotlin_method_call_chain_wrap = normal ij_kotlin_method_parameters_new_line_after_left_paren = true ij_kotlin_method_parameters_right_paren_on_new_line = true -ij_kotlin_method_parameters_wrap = on_every_item ij_kotlin_name_count_to_use_star_import = 9999 ij_kotlin_name_count_to_use_star_import_for_members = 9999 ij_kotlin_parameter_annotation_wrap = off @@ -82,4 +82,16 @@ ij_kotlin_variable_annotation_wrap = off ij_kotlin_while_on_new_line = false ij_kotlin_wrap_elvis_expressions = 1 ij_kotlin_wrap_expression_body_functions = 1 -ij_kotlin_wrap_first_method_in_call_chain = false \ No newline at end of file +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/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..81783ca6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: ci-android + +on: + workflow_dispatch: + pull_request: + +jobs: + format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Run ktlint + run: ./gradlew ktlintCheck diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9b911055..127f0d5a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,192 +1,202 @@ 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) } android { - namespace = Constants.APP_ID - compileSdk = Constants.TARGET_SDK + namespace = Constants.APP_ID + compileSdk = Constants.TARGET_SDK - androidResources { - generateLocaleConfig = true - } + androidResources { + generateLocaleConfig = true + } - defaultConfig { - applicationId = Constants.APP_ID - minSdk = Constants.MIN_SDK - targetSdk = Constants.TARGET_SDK - versionCode = determineVersionCode() - versionName = determineVersionName() + defaultConfig { + applicationId = Constants.APP_ID + minSdk = Constants.MIN_SDK + targetSdk = Constants.TARGET_SDK + versionCode = determineVersionCode() + 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 + } - 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) - } - debug { isDebuggable = true } + release { + isDebuggable = false + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + signingConfig = signingConfigs.getByName(Constants.RELEASE) + } + debug { isDebuggable = true } - create(Constants.NIGHTLY) { - initWith(buildTypes.getByName(Constants.RELEASE)) - } + create(Constants.NIGHTLY) { + initWith(buildTypes.getByName(Constants.RELEASE)) + } - 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}" } } } val generalImplementation by configurations dependencies { - implementation(project(":logcatter")) + implementation(project(":logcatter")) - 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) + // 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) - // 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) - // get tunnel lib from github packages or mavenLocal - implementation(libs.tunnel) - implementation(libs.amneziawg.android) - coreLibraryDesugaring(libs.desugar.jdk.libs) + // get tunnel lib from github packages or mavenLocal + 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) + implementation(libs.zaneschepke.multifab) - implementation(libs.zaneschepke.multifab) + // hilt + implementation(libs.hilt.android) + ksp(libs.hilt.android.compiler) - // hilt - implementation(libs.hilt.android) - ksp(libs.hilt.android.compiler) + // accompanist + implementation(libs.accompanist.permissions) + implementation(libs.accompanist.flowlayout) + implementation(libs.accompanist.drawablepainter) - // accompanist - implementation(libs.accompanist.permissions) - implementation(libs.accompanist.flowlayout) - 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) + // icons + implementation(libs.material.icons.extended) + // serialization + implementation(libs.kotlinx.serialization.json) - // icons - implementation(libs.material.icons.extended) - // serialization - implementation(libs.kotlinx.serialization.json) + // barcode scanning + implementation(libs.zxing.android.embedded) - // barcode scanning - implementation(libs.zxing.android.embedded) + // 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) + implementation(libs.androidx.core.google.shortcuts) - // shortcuts - implementation(libs.androidx.core) - implementation(libs.androidx.core.google.shortcuts) - - // splash - implementation(libs.androidx.core.splashscreen) + // splash + implementation(libs.androidx.core.splashscreen) } fun determineVersionCode(): Int { - return if(isNightlyBuild()) Constants.VERSION_CODE + Constants.NIGHTLY_CODE else Constants.VERSION_CODE + return if (isNightlyBuild()) { + Constants.VERSION_CODE + + Constants.NIGHTLY_CODE + } else { + Constants.VERSION_CODE + } } fun determineVersionName(): String { - return if(isNightlyBuild()) Constants.VERSION_NAME + "-${grgitService.service.get().grgit.head().abbreviatedId}" else Constants.VERSION_NAME + return if (isNightlyBuild()) { + Constants.VERSION_NAME + + "-${grgitService.service.get().grgit.head().abbreviatedId}" + } else { + Constants.VERSION_NAME + } } diff --git a/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/ExampleInstrumentedTest.kt index 340f9041..e7789473 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 c15965d9..ddad9306 100644 --- a/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/MigrationTest.kt +++ b/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/MigrationTest.kt @@ -12,33 +12,33 @@ 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/WireGuardAutoTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt index 803abb1c..e47581b1 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt @@ -14,44 +14,44 @@ import timber.log.Timber @HiltAndroidApp class WireGuardAutoTunnel : Application() { + override fun onCreate() { + super.onCreate() + instance = this + 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 - if (BuildConfig.DEBUG) { - Timber.plant(Timber.DebugTree()) - StrictMode.setThreadPolicy( - ThreadPolicy.Builder() - .detectDiskReads() - .detectDiskWrites() - .detectNetwork() - .penaltyLog() - .build(), - ) - } else Timber.plant(ReleaseTree()) - } + companion object { + lateinit var instance: WireGuardAutoTunnel + private set - companion object { + fun isRunningOnAndroidTv(): Boolean { + return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) + } - lateinit var instance: WireGuardAutoTunnel - private set + fun requestTunnelTileServiceStateUpdate() { + TileService.requestListeningState( + instance, + ComponentName(instance, TunnelControlTile::class.java), + ) + } - fun isRunningOnAndroidTv(): Boolean { - return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) - } - - fun requestTunnelTileServiceStateUpdate() { - TileService.requestListeningState( - instance, - ComponentName(instance, TunnelControlTile::class.java), - ) - } - - fun requestAutoTunnelTileServiceUpdate() { - TileService.requestListeningState( - instance, - ComponentName(instance, AutoTunnelControlTile::class.java), - ) - } - } + fun requestAutoTunnelTileServiceUpdate() { + TileService.requestListeningState( + instance, + ComponentName(instance, AutoTunnelControlTile::class.java), + ) + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt index 75e9fe33..721617e9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt @@ -10,46 +10,46 @@ import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig @Database( - entities = [Settings::class, TunnelConfig::class], - version = 8, - 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), - ], - exportSchema = true, + entities = [Settings::class, TunnelConfig::class], + version = 8, + 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), + ], + 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", + tableName = "Settings", + columnName = "default_tunnel", ) @DeleteColumn( - tableName = "Settings", - columnName = "is_battery_saver_enabled", + tableName = "Settings", + columnName = "is_battery_saver_enabled", ) class RemoveLegacySettingColumnsMigration : AutoMigrationSpec 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 1f8a9a25..9b8f4ef1 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,17 @@ 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 8d76569d..ecdd6777 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/DatabaseListConverters.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/DatabaseListConverters.kt @@ -5,20 +5,20 @@ import kotlinx.serialization.encodeToString 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 833fc007..3cebf1ac 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/Queries.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/Queries.kt @@ -1,35 +1,35 @@ package com.zaneschepke.wireguardautotunnel.data object Queries { - fun createDefaultSettings(): String { - return """ - INSERT INTO Settings (is_tunnel_enabled, - is_tunnel_on_mobile_data_enabled, - trusted_network_ssids, - is_always_on_vpn_enabled, - is_tunnel_on_ethernet_enabled, - is_shortcuts_enabled, - is_tunnel_on_wifi_enabled, - is_kernel_enabled, - is_restore_on_boot_enabled, - is_multi_tunnel_enabled) - VALUES - ('false', - 'false', - 'sampleSSID1,sampleSSID2', - 'false', - 'false', - 'false', - 'false', - 'false', - 'false', - 'false') - """.trimIndent() - } + fun createDefaultSettings(): String { + return """ + INSERT INTO Settings (is_tunnel_enabled, + is_tunnel_on_mobile_data_enabled, + trusted_network_ssids, + is_always_on_vpn_enabled, + is_tunnel_on_ethernet_enabled, + is_shortcuts_enabled, + is_tunnel_on_wifi_enabled, + is_kernel_enabled, + is_restore_on_boot_enabled, + is_multi_tunnel_enabled) + VALUES + ('false', + 'false', + 'sampleSSID1,sampleSSID2', + 'false', + 'false', + 'false', + 'false', + 'false', + 'false', + 'false') + """.trimIndent() + } - fun createTunnelConfig(): String { - return """ - INSERT INTO TunnelConfig (name, wg_quick) VALUES ('test', 'test') - """.trimIndent() - } + fun createTunnelConfig(): String { + return """ + INSERT INTO TunnelConfig (name, wg_quick) VALUES ('test', 'test') + """.trimIndent() + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/SettingsDao.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/SettingsDao.kt index 321a6c9e..b77cbd45 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/SettingsDao.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/SettingsDao.kt @@ -10,27 +10,27 @@ import kotlinx.coroutines.flow.Flow @Dao interface SettingsDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(t: Settings) + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun save(t: Settings) - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun saveAll(t: List) + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun saveAll(t: List) - @Query("SELECT * FROM settings WHERE id=:id") - suspend fun getById(id: Long): Settings? + @Query("SELECT * FROM settings WHERE id=:id") + suspend fun getById(id: Long): Settings? - @Query("SELECT * FROM settings") - suspend fun getAll(): List + @Query("SELECT * FROM settings") + suspend fun getAll(): List - @Query("SELECT * FROM settings LIMIT 1") - fun getSettingsFlow(): Flow + @Query("SELECT * FROM settings LIMIT 1") + fun getSettingsFlow(): Flow - @Query("SELECT * FROM settings") - fun getAllFlow(): Flow> + @Query("SELECT * FROM settings") + fun getAllFlow(): Flow> - @Delete - suspend fun delete(t: Settings) + @Delete + suspend fun delete(t: Settings) - @Query("SELECT COUNT('id') FROM settings") - suspend fun count(): Long + @Query("SELECT COUNT('id') FROM settings") + suspend fun count(): Long } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/TunnelConfigDao.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/TunnelConfigDao.kt index 1946c747..00408085 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/TunnelConfigDao.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/TunnelConfigDao.kt @@ -11,42 +11,42 @@ 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") - 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("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/datastore/DataStoreManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/DataStoreManager.kt index b43e2b3b..a608d929 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/DataStoreManager.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/DataStoreManager.kt @@ -18,66 +18,64 @@ 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 LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN") - val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN") - val TUNNEL_RUNNING_FROM_MANUAL_START = - booleanPreferencesKey("TUNNEL_RUNNING_FROM_MANUAL_START") - val ACTIVE_TUNNEL = intPreferencesKey("ACTIVE_TUNNEL") - val CURRENT_SSID = stringPreferencesKey("CURRENT_SSID") - val IS_PIN_LOCK_ENABLED = booleanPreferencesKey("PIN_LOCK_ENABLED") - } + companion object { + val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN") + val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN") + val TUNNEL_RUNNING_FROM_MANUAL_START = + booleanPreferencesKey("TUNNEL_RUNNING_FROM_MANUAL_START") + val ACTIVE_TUNNEL = intPreferencesKey("ACTIVE_TUNNEL") + val CURRENT_SSID = stringPreferencesKey("CURRENT_SSID") + val IS_PIN_LOCK_ENABLED = booleanPreferencesKey("PIN_LOCK_ENABLED") + } - // 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.e(e) - } - } - } + suspend fun init() { + withContext(ioDispatcher) { + try { + context.dataStore.data.first() + } catch (e: IOException) { + Timber.e(e) + } + } + } - suspend fun saveToDataStore(key: Preferences.Key, value: T) { - withContext(ioDispatcher) { - try { - context.dataStore.edit { it[key] = value } - } catch (e: IOException) { - Timber.e(e) - } catch (e: Exception) { - Timber.e(e) - } - } - } + suspend fun saveToDataStore(key: Preferences.Key, value: T) { + withContext(ioDispatcher) { + try { + context.dataStore.edit { it[key] = value } + } catch (e: IOException) { + Timber.e(e) + } catch (e: Exception) { + Timber.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.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.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 + val preferencesFlow: Flow = context.dataStore.data } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/GeneralState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/GeneralState.kt index 19655ff0..33ada272 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/GeneralState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/GeneralState.kt @@ -1,16 +1,16 @@ package com.zaneschepke.wireguardautotunnel.data.domain data class GeneralState( - val isLocationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT, - val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT, - val isTunnelRunningFromManualStart: Boolean = TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT, - val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT, - val activeTunnelId: Int? = null + val isLocationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT, + val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT, + val isTunnelRunningFromManualStart: Boolean = TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT, + val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT, + val activeTunnelId: Int? = null, ) { - companion object { - const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false - const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false - const val TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT = false - const val PIN_LOCK_ENABLED_DEFAULT = false - } + companion object { + const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false + const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false + const val TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT = false + const val PIN_LOCK_ENABLED_DEFAULT = false + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/Settings.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/Settings.kt index 450feab7..bec24e96 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/Settings.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/Settings.kt @@ -6,53 +6,53 @@ import androidx.room.PrimaryKey @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_auto_tunnel_paused", - defaultValue = "false", - ) - val isAutoTunnelPaused: Boolean = false, - @ColumnInfo( - name = "is_ping_enabled", - defaultValue = "false", - ) - val isPingEnabled: Boolean = false, - @ColumnInfo( - name = "is_amnezia_enabled", - defaultValue = "false", - ) - val isAmneziaEnabled: 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_auto_tunnel_paused", + defaultValue = "false", + ) + val isAutoTunnelPaused: Boolean = false, + @ColumnInfo( + name = "is_ping_enabled", + defaultValue = "false", + ) + val isPingEnabled: Boolean = false, + @ColumnInfo( + name = "is_amnezia_enabled", + defaultValue = "false", + ) + val isAmneziaEnabled: Boolean = false, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt index efc6f5c4..77a92385 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt @@ -9,45 +9,45 @@ import java.io.InputStream @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, + @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, ) { - companion object { - fun configFromWgQuick(wgQuick: String): Config { - val inputStream: InputStream = wgQuick.byteInputStream() - return inputStream.bufferedReader(Charsets.UTF_8).use { - Config.parse(it) - } - } + companion object { + fun configFromWgQuick(wgQuick: String): Config { + val inputStream: InputStream = wgQuick.byteInputStream() + return inputStream.bufferedReader(Charsets.UTF_8).use { + Config.parse(it) + } + } - fun configFromAmQuick(amQuick: String): org.amnezia.awg.config.Config { - val inputStream: InputStream = amQuick.byteInputStream() - return inputStream.bufferedReader(Charsets.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(Charsets.UTF_8).use { + org.amnezia.awg.config.Config.parse(it) + } + } - const val AM_QUICK_DEFAULT = "" - } + const val AM_QUICK_DEFAULT = "" + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRepository.kt index e10e4f98..9daf9a9c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRepository.kt @@ -3,12 +3,13 @@ package com.zaneschepke.wireguardautotunnel.data.repository import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig interface AppDataRepository { - suspend fun getPrimaryOrFirstTunnel(): TunnelConfig? - suspend fun getStartTunnelConfig(): TunnelConfig? + suspend fun getPrimaryOrFirstTunnel(): TunnelConfig? - suspend fun toggleWatcherServicePause() + suspend fun getStartTunnelConfig(): TunnelConfig? - val settings: SettingsRepository - val tunnels: TunnelConfigRepository - val appState: AppStateRepository + suspend fun toggleWatcherServicePause() + + val settings: SettingsRepository + val tunnels: TunnelConfigRepository + val appState: AppStateRepository } 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 009b2f08..1214d824 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 @@ -3,32 +3,36 @@ package com.zaneschepke.wireguardautotunnel.data.repository import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import javax.inject.Inject -class AppDataRoomRepository @Inject constructor( - override val settings: SettingsRepository, - override val tunnels: TunnelConfigRepository, - override val appState: AppStateRepository +class AppDataRoomRepository +@Inject +constructor( + override val settings: SettingsRepository, + override val tunnels: TunnelConfigRepository, + override val appState: AppStateRepository, ) : AppDataRepository { - override suspend fun getPrimaryOrFirstTunnel(): TunnelConfig? { - return tunnels.findPrimary().firstOrNull() ?: tunnels.getAll().firstOrNull() - } + override suspend fun getPrimaryOrFirstTunnel(): TunnelConfig? { + return tunnels.findPrimary().firstOrNull() ?: tunnels.getAll().firstOrNull() + } - override suspend fun getStartTunnelConfig(): TunnelConfig? { - return if (appState.isTunnelRunningFromManualStart()) { - appState.getActiveTunnelId()?.let { - tunnels.getById(it) - } - } else null - } + override suspend fun getStartTunnelConfig(): TunnelConfig? { + return if (appState.isTunnelRunningFromManualStart()) { + appState.getActiveTunnelId()?.let { + tunnels.getById(it) + } + } else { + null + } + } - override suspend fun toggleWatcherServicePause() { - val settings = settings.getSettings() - if (settings.isAutoTunnelEnabled) { - val pauseAutoTunnel = !settings.isAutoTunnelPaused - this.settings.save( - settings.copy( - isAutoTunnelPaused = pauseAutoTunnel, - ), - ) - } - } + override suspend fun toggleWatcherServicePause() { + val settings = settings.getSettings() + if (settings.isAutoTunnelEnabled) { + val pauseAutoTunnel = !settings.isAutoTunnelPaused + this.settings.save( + settings.copy( + isAutoTunnelPaused = pauseAutoTunnel, + ), + ) + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppStateRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppStateRepository.kt index 97505b05..578bb8d1 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppStateRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppStateRepository.kt @@ -4,26 +4,29 @@ import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState import kotlinx.coroutines.flow.Flow interface AppStateRepository { - suspend fun isLocationDisclosureShown(): Boolean - suspend fun setLocationDisclosureShown(shown: Boolean) + suspend fun isLocationDisclosureShown(): Boolean - suspend fun isPinLockEnabled(): Boolean - suspend fun setPinLockEnabled(enabled: Boolean) + suspend fun setLocationDisclosureShown(shown: Boolean) - suspend fun isBatteryOptimizationDisableShown(): Boolean - suspend fun setBatteryOptimizationDisableShown(shown: Boolean) + suspend fun isPinLockEnabled(): Boolean - suspend fun isTunnelRunningFromManualStart(): Boolean - suspend fun setTunnelRunningFromManualStart(id: Int) + suspend fun setPinLockEnabled(enabled: Boolean) - suspend fun setManualStop() + suspend fun isBatteryOptimizationDisableShown(): Boolean - suspend fun getActiveTunnelId(): Int? + suspend fun setBatteryOptimizationDisableShown(shown: Boolean) - suspend fun getCurrentSsid(): String? + suspend fun isTunnelRunningFromManualStart(): Boolean - suspend fun setCurrentSsid(ssid: String) + suspend fun setTunnelRunningFromManualStart(id: Int) - val generalStateFlow: Flow + suspend fun setManualStop() + suspend fun getActiveTunnelId(): Int? + + suspend fun getCurrentSsid(): String? + + suspend fun setCurrentSsid(ssid: String) + + val generalStateFlow: Flow } 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 4a73f681..a1301cf0 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 @@ -7,86 +7,90 @@ 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.LOCATION_DISCLOSURE_SHOWN) - ?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT - } + AppStateRepository { + override suspend fun isLocationDisclosureShown(): Boolean { + return dataStoreManager.getFromStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN) + ?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT + } - override suspend fun setLocationDisclosureShown(shown: Boolean) { - dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, shown) - } + override suspend fun setLocationDisclosureShown(shown: Boolean) { + dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, shown) + } - override suspend fun isPinLockEnabled(): Boolean { - return dataStoreManager.getFromStore(DataStoreManager.IS_PIN_LOCK_ENABLED) - ?: GeneralState.PIN_LOCK_ENABLED_DEFAULT - } + override suspend fun isPinLockEnabled(): Boolean { + return dataStoreManager.getFromStore(DataStoreManager.IS_PIN_LOCK_ENABLED) + ?: GeneralState.PIN_LOCK_ENABLED_DEFAULT + } - override suspend fun setPinLockEnabled(enabled: Boolean) { - dataStoreManager.saveToDataStore(DataStoreManager.IS_PIN_LOCK_ENABLED, enabled) - } + override suspend fun setPinLockEnabled(enabled: Boolean) { + dataStoreManager.saveToDataStore(DataStoreManager.IS_PIN_LOCK_ENABLED, enabled) + } - override suspend fun isBatteryOptimizationDisableShown(): Boolean { - return dataStoreManager.getFromStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN) - ?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT - } + override suspend fun isBatteryOptimizationDisableShown(): Boolean { + return dataStoreManager.getFromStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN) + ?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT + } - override suspend fun setBatteryOptimizationDisableShown(shown: Boolean) { - dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, shown) - } + override suspend fun setBatteryOptimizationDisableShown(shown: Boolean) { + dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, shown) + } - override suspend fun isTunnelRunningFromManualStart(): Boolean { - return dataStoreManager.getFromStore(DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START) - ?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT - } + override suspend fun isTunnelRunningFromManualStart(): Boolean { + return dataStoreManager.getFromStore(DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START) + ?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT + } - override suspend fun setTunnelRunningFromManualStart(id: Int) { - setTunnelRunningFromManualStart(true) - setActiveTunnelId(id) - } + override suspend fun setTunnelRunningFromManualStart(id: Int) { + setTunnelRunningFromManualStart(true) + setActiveTunnelId(id) + } - override suspend fun setManualStop() { - setTunnelRunningFromManualStart(false) - } + override suspend fun setManualStop() { + setTunnelRunningFromManualStart(false) + } - private suspend fun setTunnelRunningFromManualStart(running: Boolean) { - dataStoreManager.saveToDataStore(DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START, running) - } + private suspend fun setTunnelRunningFromManualStart(running: Boolean) { + dataStoreManager.saveToDataStore(DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START, running) + } - override suspend fun getActiveTunnelId(): Int? { - return dataStoreManager.getFromStore(DataStoreManager.ACTIVE_TUNNEL) - } + override suspend fun getActiveTunnelId(): Int? { + return dataStoreManager.getFromStore(DataStoreManager.ACTIVE_TUNNEL) + } - private suspend fun setActiveTunnelId(id: Int) { - dataStoreManager.saveToDataStore(DataStoreManager.ACTIVE_TUNNEL, id) - } + private suspend fun setActiveTunnelId(id: Int) { + dataStoreManager.saveToDataStore(DataStoreManager.ACTIVE_TUNNEL, id) + } - override suspend fun getCurrentSsid(): String? { - return dataStoreManager.getFromStore(DataStoreManager.CURRENT_SSID) - } + override suspend fun getCurrentSsid(): String? { + return dataStoreManager.getFromStore(DataStoreManager.CURRENT_SSID) + } - override suspend fun setCurrentSsid(ssid: String) { - dataStoreManager.saveToDataStore(DataStoreManager.CURRENT_SSID, ssid) - } + override suspend fun setCurrentSsid(ssid: String) { + dataStoreManager.saveToDataStore(DataStoreManager.CURRENT_SSID, ssid) + } - override val generalStateFlow: Flow = - dataStoreManager.preferencesFlow.map { prefs -> - prefs?.let { pref -> - try { - GeneralState( - isLocationDisclosureShown = pref[DataStoreManager.LOCATION_DISCLOSURE_SHOWN] - ?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT, - isBatteryOptimizationDisableShown = pref[DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN] - ?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT, - isTunnelRunningFromManualStart = pref[DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START] - ?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT, - isPinLockEnabled = pref[DataStoreManager.IS_PIN_LOCK_ENABLED] - ?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT, - ) - } catch (e: IllegalArgumentException) { - Timber.e(e) - GeneralState() - } - } ?: GeneralState() - } + override val generalStateFlow: Flow = + dataStoreManager.preferencesFlow.map { prefs -> + prefs?.let { pref -> + try { + GeneralState( + isLocationDisclosureShown = + pref[DataStoreManager.LOCATION_DISCLOSURE_SHOWN] + ?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT, + isBatteryOptimizationDisableShown = + pref[DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN] + ?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT, + isTunnelRunningFromManualStart = + pref[DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START] + ?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT, + isPinLockEnabled = + pref[DataStoreManager.IS_PIN_LOCK_ENABLED] + ?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT, + ) + } catch (e: IllegalArgumentException) { + Timber.e(e) + GeneralState() + } + } ?: GeneralState() + } } 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 d218aa5b..de7abe16 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 @@ -5,20 +5,19 @@ import com.zaneschepke.wireguardautotunnel.data.domain.Settings import kotlinx.coroutines.flow.Flow class RoomSettingsRepository(private val settingsDoa: SettingsDao) : SettingsRepository { + override suspend fun save(settings: Settings) { + settingsDoa.save(settings) + } - override suspend fun save(settings: Settings) { - settingsDoa.save(settings) - } + override fun getSettingsFlow(): Flow { + return settingsDoa.getSettingsFlow() + } - override fun getSettingsFlow(): Flow { - return settingsDoa.getSettingsFlow() - } + override suspend fun getSettings(): Settings { + return settingsDoa.getAll().firstOrNull() ?: Settings() + } - override suspend fun getSettings(): Settings { - return settingsDoa.getAll().firstOrNull() ?: Settings() - } - - override suspend fun getAll(): List { - return settingsDoa.getAll() - } + override suspend fun getAll(): List { + return settingsDoa.getAll() + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomTunnelConfigRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomTunnelConfigRepository.kt index ac36a044..b0a62351 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomTunnelConfigRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomTunnelConfigRepository.kt @@ -6,67 +6,66 @@ import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs import kotlinx.coroutines.flow.Flow class RoomTunnelConfigRepository(private val tunnelConfigDao: TunnelConfigDao) : - TunnelConfigRepository { - override fun getTunnelConfigsFlow(): Flow { - return tunnelConfigDao.getAllFlow() - } + TunnelConfigRepository { + override fun getTunnelConfigsFlow(): Flow { + return tunnelConfigDao.getAllFlow() + } - override suspend fun getAll(): TunnelConfigs { - return tunnelConfigDao.getAll() - } + override suspend fun getAll(): TunnelConfigs { + return tunnelConfigDao.getAll() + } - override suspend fun save(tunnelConfig: TunnelConfig) { - tunnelConfigDao.save(tunnelConfig) - } + override suspend fun save(tunnelConfig: TunnelConfig) { + tunnelConfigDao.save(tunnelConfig) + } - override suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?) { - tunnelConfigDao.resetPrimaryTunnel() - tunnelConfig?.let { - save( - it.copy( - isPrimaryTunnel = true, - ), - ) - } + override suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?) { + tunnelConfigDao.resetPrimaryTunnel() + tunnelConfig?.let { + save( + it.copy( + isPrimaryTunnel = true, + ), + ) + } + } - } + override suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?) { + tunnelConfigDao.resetMobileDataTunnel() + tunnelConfig?.let { + save( + it.copy( + isMobileDataTunnel = true, + ), + ) + } + } - override suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?) { - tunnelConfigDao.resetMobileDataTunnel() - tunnelConfig?.let { - save( - it.copy( - isMobileDataTunnel = true, - ), - ) - } - } + override suspend fun delete(tunnelConfig: TunnelConfig) { + tunnelConfigDao.delete(tunnelConfig) + } - override suspend fun delete(tunnelConfig: TunnelConfig) { - tunnelConfigDao.delete(tunnelConfig) - } + override suspend fun getById(id: Int): TunnelConfig? { + return tunnelConfigDao.getById(id.toLong()) + } - override suspend fun getById(id: Int): TunnelConfig? { - return tunnelConfigDao.getById(id.toLong()) - } + override suspend fun count(): Int { + return tunnelConfigDao.count().toInt() + } - override suspend fun count(): Int { - return tunnelConfigDao.count().toInt() - } + override suspend fun findByTunnelName(name: String): TunnelConfig? { + return tunnelConfigDao.getByName(name) + } - override suspend fun findByTunnelName(name: String): TunnelConfig? { - return tunnelConfigDao.getByName(name) - } + override suspend fun findByTunnelNetworksName(name: String): TunnelConfigs { + return tunnelConfigDao.findByTunnelNetworkName(name) + } - override suspend fun findByTunnelNetworksName(name: String): TunnelConfigs { - return tunnelConfigDao.findByTunnelNetworkName(name) - } + override suspend fun findByMobileDataTunnel(): TunnelConfigs { + return tunnelConfigDao.findByMobileDataTunnel() + } - override suspend fun findByMobileDataTunnel(): TunnelConfigs { - return tunnelConfigDao.findByMobileDataTunnel() - } - - override suspend fun findPrimary(): TunnelConfigs { - return tunnelConfigDao.findByPrimary() - } + override suspend fun findPrimary(): TunnelConfigs { + return tunnelConfigDao.findByPrimary() + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/SettingsRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/SettingsRepository.kt index e4250f9c..c0f03d5d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/SettingsRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/SettingsRepository.kt @@ -4,11 +4,11 @@ import com.zaneschepke.wireguardautotunnel.data.domain.Settings import kotlinx.coroutines.flow.Flow interface SettingsRepository { - suspend fun save(settings: Settings) + suspend fun save(settings: Settings) - fun getSettingsFlow(): Flow + fun getSettingsFlow(): Flow - suspend fun getSettings(): Settings + suspend fun getSettings(): Settings - suspend fun getAll(): List + suspend fun getAll(): List } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepository.kt index 0261322f..bb19aa83 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepository.kt @@ -5,28 +5,27 @@ import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs import kotlinx.coroutines.flow.Flow interface TunnelConfigRepository { + fun getTunnelConfigsFlow(): Flow - fun getTunnelConfigsFlow(): Flow + suspend fun getAll(): TunnelConfigs - suspend fun getAll(): TunnelConfigs + suspend fun save(tunnelConfig: TunnelConfig) - suspend fun save(tunnelConfig: TunnelConfig) + suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?) - suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?) + suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?) - suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?) + suspend fun delete(tunnelConfig: TunnelConfig) - suspend fun delete(tunnelConfig: TunnelConfig) + suspend fun getById(id: Int): TunnelConfig? - suspend fun getById(id: Int): TunnelConfig? + suspend fun count(): Int - suspend fun count(): Int + suspend fun findByTunnelName(name: String): TunnelConfig? - suspend fun findByTunnelName(name: String): TunnelConfig? + suspend fun findByTunnelNetworksName(name: String): TunnelConfigs - suspend fun findByTunnelNetworksName(name: String): TunnelConfigs + suspend fun findByMobileDataTunnel(): TunnelConfigs - suspend fun findByMobileDataTunnel(): TunnelConfigs - - suspend fun findPrimary(): TunnelConfigs + suspend fun findPrimary(): TunnelConfigs } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/AppModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/AppModule.kt index 788a052d..cfca31fa 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/AppModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/AppModule.kt @@ -2,7 +2,7 @@ package com.zaneschepke.wireguardautotunnel.module import android.content.Context import com.zaneschepke.logcatter.LocalLogCollector -import com.zaneschepke.logcatter.LogcatHelper +import com.zaneschepke.logcatter.LogcatUtil import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -16,15 +16,15 @@ 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): LocalLogCollector { - return LogcatHelper.init(context = context) - } + @Singleton + @Provides + fun provideLogCollect(@ApplicationContext context: Context): LocalLogCollector { + return LogcatUtil.init(context = context) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/CoroutinesDispatchersModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/CoroutinesDispatchersModule.kt index 40f1063d..f28d8cee 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/CoroutinesDispatchersModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/CoroutinesDispatchersModule.kt @@ -10,19 +10,19 @@ 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/module/RepositoryModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/RepositoryModule.kt index a3f2e289..a0476cfe 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/RepositoryModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/RepositoryModule.kt @@ -27,67 +27,62 @@ import javax.inject.Singleton @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): TunnelConfigRepository { - return RoomTunnelConfigRepository(tunnelConfigDao) - } + @Singleton + @Provides + fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao): TunnelConfigRepository { + return RoomTunnelConfigRepository(tunnelConfigDao) + } - @Singleton - @Provides - fun provideSettingsRepository(settingsDao: SettingsDao): SettingsRepository { - return RoomSettingsRepository(settingsDao) - } + @Singleton + @Provides + fun provideSettingsRepository(settingsDao: SettingsDao): SettingsRepository { + return RoomSettingsRepository(settingsDao) + } - @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 provideAppDataRepository( - settingsRepository: SettingsRepository, - tunnelConfigRepository: TunnelConfigRepository, - appStateRepository: AppStateRepository - ): AppDataRepository { - return AppDataRoomRepository(settingsRepository, tunnelConfigRepository, appStateRepository) - } + @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 provideAppDataRepository( + settingsRepository: SettingsRepository, + tunnelConfigRepository: TunnelConfigRepository, + appStateRepository: AppStateRepository, + ): AppDataRepository { + return AppDataRoomRepository(settingsRepository, tunnelConfigRepository, appStateRepository) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ServiceModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ServiceModule.kt index 3a438b9e..3810bb2a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ServiceModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ServiceModule.kt @@ -15,25 +15,19 @@ import dagger.hilt.android.scopes.ServiceScoped @Module @InstallIn(ServiceComponent::class) abstract class ServiceModule { - @Binds - @ServiceScoped - abstract fun provideNotificationService( - wireGuardNotification: WireGuardNotification - ): NotificationService + @Binds + @ServiceScoped + abstract fun provideNotificationService(wireGuardNotification: WireGuardNotification): NotificationService - @Binds - @ServiceScoped - abstract fun provideWifiService(wifiService: WifiService): NetworkService + @Binds + @ServiceScoped + abstract fun provideWifiService(wifiService: WifiService): NetworkService - @Binds - @ServiceScoped - abstract fun provideMobileDataService( - mobileDataService: MobileDataService - ): NetworkService + @Binds + @ServiceScoped + abstract fun provideMobileDataService(mobileDataService: MobileDataService): NetworkService - @Binds - @ServiceScoped - abstract fun provideEthernetService( - ethernetService: EthernetService - ): NetworkService + @Binds + @ServiceScoped + abstract fun provideEthernetService(ethernetService: EthernetService): NetworkService } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt index 26af2753..06666e1a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt @@ -23,58 +23,55 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) class TunnelModule { - @Provides - @Singleton - fun provideRootShell(@ApplicationContext context: Context): RootShell { - return RootShell(context) - } + @Provides + @Singleton + fun provideRootShell(@ApplicationContext context: Context): RootShell { + return RootShell(context) + } - @Provides - @Singleton - @Userspace - fun provideUserspaceBackend(@ApplicationContext context: Context): Backend { - return GoBackend(context) - } + @Provides + @Singleton + @Userspace + fun provideUserspaceBackend(@ApplicationContext context: Context): Backend { + return GoBackend(context) + } - @Provides - @Singleton - @Kernel - fun provideKernelBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend { - return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell)) - } + @Provides + @Singleton + @Kernel + fun provideKernelBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend { + return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell)) + } - @Provides - @Singleton - fun provideAmneziaBackend(@ApplicationContext context: Context): org.amnezia.awg.backend.Backend { - return org.amnezia.awg.backend.GoBackend(context) - } + @Provides + @Singleton + fun provideAmneziaBackend(@ApplicationContext context: Context): org.amnezia.awg.backend.Backend { + return org.amnezia.awg.backend.GoBackend(context) + } - @Provides - @Singleton - fun provideVpnService( - amneziaBackend: Provider, - @Userspace userspaceBackend: Provider, - @Kernel kernelBackend: Provider, - appDataRepository: AppDataRepository, - @ApplicationScope applicationScope: CoroutineScope, - @IoDispatcher ioDispatcher: CoroutineDispatcher - ): VpnService { - return WireGuardTunnel( - amneziaBackend, - userspaceBackend, - kernelBackend, - appDataRepository, - applicationScope, - ioDispatcher, - ) - } + @Provides + @Singleton + fun provideVpnService( + amneziaBackend: Provider, + @Userspace userspaceBackend: Provider, + @Kernel kernelBackend: Provider, + appDataRepository: AppDataRepository, + @ApplicationScope applicationScope: CoroutineScope, + @IoDispatcher ioDispatcher: CoroutineDispatcher, + ): VpnService { + return WireGuardTunnel( + amneziaBackend, + userspaceBackend, + kernelBackend, + appDataRepository, + applicationScope, + ioDispatcher, + ) + } - @Provides - @Singleton - fun provideServiceManager( - appDataRepository: AppDataRepository, - @IoDispatcher ioDispatcher: CoroutineDispatcher - ): ServiceManager { - return ServiceManager(appDataRepository, ioDispatcher) - } + @Provides + @Singleton + fun provideServiceManager(appDataRepository: AppDataRepository, @IoDispatcher ioDispatcher: CoroutineDispatcher): ServiceManager { + return ServiceManager(appDataRepository, ioDispatcher) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ViewModelModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ViewModelModule.kt index 4cbb42e6..41b2aaf7 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ViewModelModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ViewModelModule.kt @@ -13,13 +13,9 @@ 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/receiver/BootReceiver.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt index cc1986cd..d214e195 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt @@ -14,43 +14,42 @@ import javax.inject.Inject @AndroidEntryPoint class BootReceiver : BroadcastReceiver() { + @Inject + lateinit var appDataRepository: AppDataRepository - @Inject - lateinit var appDataRepository: AppDataRepository + @Inject + lateinit var serviceManager: ServiceManager - @Inject - lateinit var serviceManager: ServiceManager + @Inject + @ApplicationScope + lateinit var applicationScope: CoroutineScope - @Inject - @ApplicationScope - lateinit var applicationScope: CoroutineScope - - override fun onReceive(context: Context?, intent: Intent?) { - if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return - context?.run { - applicationScope.launch { - val settings = appDataRepository.settings.getSettings() - if (settings.isRestoreOnBootEnabled) { - if (settings.isAutoTunnelEnabled) { - Timber.i("Starting watcher service from boot") - serviceManager.startWatcherServiceForeground(context) - } - if (appDataRepository.appState.isTunnelRunningFromManualStart()) { - appDataRepository.appState.getActiveTunnelId()?.let { - Timber.i("Starting tunnel that was active before reboot") - serviceManager.startVpnServiceForeground( - context, - appDataRepository.tunnels.getById(it)?.id, - ) - return@launch - } - } - if (settings.isAlwaysOnVpnEnabled) { - Timber.i("Starting vpn service from boot AOVPN") - serviceManager.startVpnServiceForeground(context) - } - } - } - } - } + override fun onReceive(context: Context?, intent: Intent?) { + if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return + context?.run { + applicationScope.launch { + val settings = appDataRepository.settings.getSettings() + if (settings.isRestoreOnBootEnabled) { + if (settings.isAutoTunnelEnabled) { + Timber.i("Starting watcher service from boot") + serviceManager.startWatcherServiceForeground(context) + } + if (appDataRepository.appState.isTunnelRunningFromManualStart()) { + appDataRepository.appState.getActiveTunnelId()?.let { + Timber.i("Starting tunnel that was active before reboot") + serviceManager.startVpnServiceForeground( + context, + appDataRepository.tunnels.getById(it)?.id, + ) + return@launch + } + } + if (settings.isAlwaysOnVpnEnabled) { + Timber.i("Starting vpn service from boot AOVPN") + serviceManager.startVpnServiceForeground(context) + } + } + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt index cad50de3..9928ed48 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt @@ -17,28 +17,28 @@ import javax.inject.Inject @AndroidEntryPoint class NotificationActionReceiver : BroadcastReceiver() { - @Inject - lateinit var settingsRepository: SettingsRepository + @Inject + lateinit var settingsRepository: SettingsRepository - @Inject - lateinit var serviceManager: ServiceManager + @Inject + lateinit var serviceManager: ServiceManager - @Inject - @ApplicationScope - lateinit var applicationScope: CoroutineScope + @Inject + @ApplicationScope + lateinit var applicationScope: CoroutineScope - override fun onReceive(context: Context, intent: Intent?) { - applicationScope.launch { - try { - //TODO fix for manual start changes when enabled - serviceManager.stopVpnServiceForeground(context) - delay(Constants.TOGGLE_TUNNEL_DELAY) - serviceManager.startVpnServiceForeground(context) - } catch (e: Exception) { - Timber.e(e) - } finally { - cancel() - } - } - } + override fun onReceive(context: Context, intent: Intent?) { + applicationScope.launch { + try { + // TODO fix for manual start changes when enabled + serviceManager.stopVpnServiceForeground(context) + delay(Constants.TOGGLE_TUNNEL_DELAY) + serviceManager.startVpnServiceForeground(context) + } catch (e: Exception) { + Timber.e(e) + } finally { + cancel() + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/Action.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/Action.kt index dc563eab..813ea4da 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/Action.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/Action.kt @@ -1,8 +1,8 @@ package com.zaneschepke.wireguardautotunnel.service.foreground enum class Action { - START, - START_FOREGROUND, - STOP, - STOP_FOREGROUND + START, + START_FOREGROUND, + STOP, + STOP_FOREGROUND, } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ForegroundService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ForegroundService.kt index 66dc33e2..908b8025 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ForegroundService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ForegroundService.kt @@ -8,49 +8,50 @@ import com.zaneschepke.wireguardautotunnel.util.Constants import timber.log.Timber open class ForegroundService : LifecycleService() { - private var isServiceStarted = false + private var isServiceStarted = false - override fun onBind(intent: Intent): IBinder? { - super.onBind(intent) - // We don't provide binding, so return null - return null - } + override fun onBind(intent: Intent): IBinder? { + super.onBind(intent) + // We don't provide binding, so return null + return null + } - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - super.onStartCommand(intent, flags, startId) - Timber.d("onStartCommand executed with startId: $startId") - if (intent != null) { - val action = intent.action - when (action) { - Action.START.name, - Action.START_FOREGROUND.name -> startService(intent.extras) + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + super.onStartCommand(intent, flags, startId) + Timber.d("onStartCommand executed with startId: $startId") + if (intent != null) { + val action = intent.action + when (action) { + Action.START.name, + Action.START_FOREGROUND.name, + -> startService(intent.extras) - Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService() - Constants.ALWAYS_ON_VPN_ACTION -> { - Timber.i("Always-on VPN starting service") - startService(intent.extras) - } + Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService() + Constants.ALWAYS_ON_VPN_ACTION -> { + Timber.i("Always-on VPN starting service") + startService(intent.extras) + } - else -> Timber.d("This should never happen. No action in the received intent") - } - } else { - Timber.d( - "with a null intent. It has been probably restarted by the system.", - ) - } - return START_STICKY - } + else -> Timber.d("This should never happen. No action in the received intent") + } + } else { + Timber.d( + "with a null intent. It has been probably restarted by the system.", + ) + } + return START_STICKY + } - protected open fun startService(extras: Bundle?) { - if (isServiceStarted) return - Timber.d("Starting ${this.javaClass.simpleName}") - isServiceStarted = true - } + protected open fun startService(extras: Bundle?) { + if (isServiceStarted) return + Timber.d("Starting ${this.javaClass.simpleName}") + isServiceStarted = true + } - protected open fun stopService() { - Timber.d("Stopping ${this.javaClass.simpleName}") - stopForeground(STOP_FOREGROUND_REMOVE) - stopSelf() - isServiceStarted = false - } + protected open fun stopService() { + Timber.d("Stopping ${this.javaClass.simpleName}") + stopForeground(STOP_FOREGROUND_REMOVE) + stopSelf() + isServiceStarted = false + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt index f825bf62..9322855b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt @@ -11,122 +11,107 @@ import kotlinx.coroutines.withContext import timber.log.Timber class ServiceManager( - private val appDataRepository: AppDataRepository, - @IoDispatcher private val ioDispatcher: CoroutineDispatcher + private val appDataRepository: AppDataRepository, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, ) { + private fun actionOnService(action: Action, context: Context, cls: Class, extras: Map? = null) { + val intent = + Intent(context, cls).also { + it.action = action.name + extras?.forEach { (k, v) -> it.putExtra(k, v) } + } + intent.component?.javaClass + try { + when (action) { + Action.START_FOREGROUND, Action.STOP_FOREGROUND -> + context.startForegroundService( + intent, + ) - private fun actionOnService( - action: Action, - context: Context, - cls: Class, - extras: Map? = null - ) { - val intent = - Intent(context, cls).also { - it.action = action.name - extras?.forEach { (k, v) -> it.putExtra(k, v) } - } - intent.component?.javaClass - try { - when (action) { - Action.START_FOREGROUND, Action.STOP_FOREGROUND -> context.startForegroundService( - intent, - ) + Action.START, Action.STOP -> context.startService(intent) + } + } catch (e: Exception) { + Timber.e(e.message) + } + } - Action.START, Action.STOP -> context.startService(intent) - } - } catch (e: Exception) { - Timber.e(e.message) - } - } + suspend fun startVpnService(context: Context, tunnelId: Int? = null, isManualStart: Boolean = false) { + if (isManualStart) onManualStart(tunnelId) + actionOnService( + Action.START, + context, + WireGuardTunnelService::class.java, + tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) }, + ) + } - suspend fun startVpnService( - context: Context, - tunnelId: Int? = null, - isManualStart: Boolean = false - ) { - if (isManualStart) onManualStart(tunnelId) - actionOnService( - Action.START, - context, - WireGuardTunnelService::class.java, - tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) }, - ) - } + suspend fun stopVpnServiceForeground(context: Context, isManualStop: Boolean = false) { + withContext(ioDispatcher) { + if (isManualStop) onManualStop() + Timber.i("Stopping vpn service") + actionOnService( + Action.STOP_FOREGROUND, + context, + WireGuardTunnelService::class.java, + ) + } + } - suspend fun stopVpnServiceForeground(context: Context, isManualStop: Boolean = false) { - withContext(ioDispatcher) { - if (isManualStop) onManualStop() - Timber.i("Stopping vpn service") - actionOnService( - Action.STOP_FOREGROUND, - context, - WireGuardTunnelService::class.java, - ) - } - } + suspend fun stopVpnService(context: Context, isManualStop: Boolean = false) { + withContext(ioDispatcher) { + if (isManualStop) onManualStop() + Timber.i("Stopping vpn service") + actionOnService( + Action.STOP, + context, + WireGuardTunnelService::class.java, + ) + } + } - suspend fun stopVpnService(context: Context, isManualStop: Boolean = false) { - withContext(ioDispatcher) { - if (isManualStop) onManualStop() - Timber.i("Stopping vpn service") - actionOnService( - Action.STOP, - context, - WireGuardTunnelService::class.java, - ) - } - } + private suspend fun onManualStop() { + appDataRepository.appState.setManualStop() + } - private suspend fun onManualStop() { - appDataRepository.appState.setManualStop() - } + private suspend fun onManualStart(tunnelId: Int?) { + tunnelId?.let { + appDataRepository.appState.setTunnelRunningFromManualStart(it) + } + } - private suspend fun onManualStart(tunnelId: Int?) { - tunnelId?.let { - appDataRepository.appState.setTunnelRunningFromManualStart(it) - } - } + suspend fun startVpnServiceForeground(context: Context, tunnelId: Int? = null, isManualStart: Boolean = false) { + withContext(ioDispatcher) { + if (isManualStart) onManualStart(tunnelId) + actionOnService( + Action.START_FOREGROUND, + context, + WireGuardTunnelService::class.java, + tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) }, + ) + } + } - suspend fun startVpnServiceForeground( - context: Context, - tunnelId: Int? = null, - isManualStart: Boolean = false - ) { - withContext(ioDispatcher) { - if (isManualStart) onManualStart(tunnelId) - actionOnService( - Action.START_FOREGROUND, - context, - WireGuardTunnelService::class.java, - tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) }, - ) - } - } + fun startWatcherServiceForeground(context: Context) { + actionOnService( + Action.START_FOREGROUND, + context, + WireGuardConnectivityWatcherService::class.java, + ) + } - fun startWatcherServiceForeground( - context: Context, - ) { - actionOnService( - Action.START_FOREGROUND, - context, - WireGuardConnectivityWatcherService::class.java, - ) - } + fun startWatcherService(context: Context) { + actionOnService( + Action.START, + context, + WireGuardConnectivityWatcherService::class.java, + ) + } - fun startWatcherService(context: Context) { - actionOnService( - Action.START, - context, - WireGuardConnectivityWatcherService::class.java, - ) - } - - fun stopWatcherService(context: Context) { - actionOnService( - Action.STOP, - context, - WireGuardConnectivityWatcherService::class.java, - ) - } + fun stopWatcherService(context: Context) { + actionOnService( + Action.STOP, + context, + WireGuardConnectivityWatcherService::class.java, + ) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WatcherState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WatcherState.kt index c4505296..d4e7158c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WatcherState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WatcherState.kt @@ -3,54 +3,71 @@ package com.zaneschepke.wireguardautotunnel.service.foreground import com.zaneschepke.wireguardautotunnel.data.domain.Settings data class WatcherState( - val isWifiConnected: Boolean = false, - val isEthernetConnected: Boolean = false, - val isMobileDataConnected: Boolean = false, - val currentNetworkSSID: String = "", - val settings: Settings = Settings() + val isWifiConnected: Boolean = false, + val isEthernetConnected: Boolean = false, + val isMobileDataConnected: Boolean = false, + val currentNetworkSSID: String = "", + val settings: Settings = Settings(), ) { - fun isEthernetConditionMet(): Boolean { - return (isEthernetConnected && - settings.isTunnelOnEthernetEnabled) - } + fun isEthernetConditionMet(): Boolean { + return ( + isEthernetConnected && + settings.isTunnelOnEthernetEnabled + ) + } - fun isMobileDataConditionMet(): Boolean { - return (!isEthernetConnected && - settings.isTunnelOnMobileDataEnabled && - !isWifiConnected && - isMobileDataConnected) - } + fun isMobileDataConditionMet(): Boolean { + return ( + !isEthernetConnected && + settings.isTunnelOnMobileDataEnabled && + !isWifiConnected && + isMobileDataConnected + ) + } - fun isTunnelOffOnMobileDataConditionMet(): Boolean { - return (!isEthernetConnected && - !settings.isTunnelOnMobileDataEnabled && - isMobileDataConnected && - !isWifiConnected) - } + fun isTunnelOffOnMobileDataConditionMet(): Boolean { + return ( + !isEthernetConnected && + !settings.isTunnelOnMobileDataEnabled && + isMobileDataConnected && + !isWifiConnected + ) + } - fun isUntrustedWifiConditionMet(): Boolean { - return (!isEthernetConnected && - isWifiConnected && - !settings.trustedNetworkSSIDs.contains(currentNetworkSSID) && - settings.isTunnelOnWifiEnabled) - } + fun isUntrustedWifiConditionMet(): Boolean { + return ( + !isEthernetConnected && + isWifiConnected && + !settings.trustedNetworkSSIDs.contains(currentNetworkSSID) && + settings.isTunnelOnWifiEnabled + ) + } - fun isTrustedWifiConditionMet(): Boolean { - return (!isEthernetConnected && - (isWifiConnected && - settings.trustedNetworkSSIDs.contains(currentNetworkSSID))) - } + fun isTrustedWifiConditionMet(): Boolean { + return ( + !isEthernetConnected && + ( + isWifiConnected && + settings.trustedNetworkSSIDs.contains(currentNetworkSSID) + ) + ) + } - fun isTunnelOffOnWifiConditionMet(): Boolean { - return (!isEthernetConnected && - (isWifiConnected && - !settings.isTunnelOnWifiEnabled)) - } + fun isTunnelOffOnWifiConditionMet(): Boolean { + return ( + !isEthernetConnected && + ( + isWifiConnected && + !settings.isTunnelOnWifiEnabled + ) + ) + } - fun isTunnelOffOnNoConnectivityMet(): Boolean { - return (!isEthernetConnected && - !isWifiConnected && - !isMobileDataConnected) - } + fun isTunnelOffOnNoConnectivityMet(): Boolean { + return ( + !isEthernetConnected && + !isWifiConnected && + !isMobileDataConnected + ) + } } - diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt index bc9c493f..9f3f4218 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt @@ -33,424 +33,442 @@ import timber.log.Timber import java.net.InetAddress import javax.inject.Inject - @AndroidEntryPoint class WireGuardConnectivityWatcherService : ForegroundService() { - private val foregroundId = 122 + private val foregroundId = 122 - @Inject - lateinit var wifiService: NetworkService + @Inject + lateinit var wifiService: NetworkService - @Inject - lateinit var mobileDataService: NetworkService + @Inject + lateinit var mobileDataService: NetworkService - @Inject - lateinit var ethernetService: NetworkService + @Inject + lateinit var ethernetService: NetworkService - @Inject - lateinit var appDataRepository: AppDataRepository + @Inject + lateinit var appDataRepository: AppDataRepository - @Inject - lateinit var notificationService: NotificationService + @Inject + lateinit var notificationService: NotificationService - @Inject - lateinit var vpnService: VpnService + @Inject + lateinit var vpnService: VpnService - @Inject - lateinit var serviceManager: ServiceManager + @Inject + lateinit var serviceManager: ServiceManager - @Inject - @IoDispatcher - lateinit var ioDispatcher: CoroutineDispatcher + @Inject + @IoDispatcher + lateinit var ioDispatcher: CoroutineDispatcher - @Inject - @MainImmediateDispatcher - lateinit var mainImmediateDispatcher: CoroutineDispatcher + @Inject + @MainImmediateDispatcher + lateinit var mainImmediateDispatcher: CoroutineDispatcher - private val networkEventsFlow = MutableStateFlow(WatcherState()) + private val networkEventsFlow = MutableStateFlow(WatcherState()) - private var watcherJob: Job? = null + private var watcherJob: Job? = null - private var wakeLock: PowerManager.WakeLock? = null - private val tag = this.javaClass.name + private var wakeLock: PowerManager.WakeLock? = null + private val tag = this.javaClass.name - override fun onCreate() { - super.onCreate() - lifecycleScope.launch(mainImmediateDispatcher) { - try { - if (appDataRepository.settings.getSettings().isAutoTunnelPaused) { - launchWatcherPausedNotification() - } else launchWatcherNotification() - } catch (e: Exception) { - Timber.e("Failed to start watcher service, not enough permissions") - } - } - } + override fun onCreate() { + super.onCreate() + lifecycleScope.launch(mainImmediateDispatcher) { + try { + if (appDataRepository.settings.getSettings().isAutoTunnelPaused) { + launchWatcherPausedNotification() + } else { + launchWatcherNotification() + } + } catch (e: Exception) { + Timber.e("Failed to start watcher service, not enough permissions") + } + } + } - override fun startService(extras: Bundle?) { - super.startService(extras) - try { - // we need this lock so our service gets not affected by Doze Mode - lifecycleScope.launch { initWakeLock() } - cancelWatcherJob() - startWatcherJob() - } catch (e: Exception) { - Timber.e("Failed to launch watcher service, no permissions") - } - } + override fun startService(extras: Bundle?) { + super.startService(extras) + try { + // we need this lock so our service gets not affected by Doze Mode + lifecycleScope.launch { initWakeLock() } + cancelWatcherJob() + startWatcherJob() + } catch (e: Exception) { + Timber.e("Failed to launch watcher service, no permissions") + } + } - override fun stopService() { - super.stopService() - wakeLock?.let { - if (it.isHeld) { - it.release() - } - } - cancelWatcherJob() - stopSelf() - } + override fun stopService() { + super.stopService() + wakeLock?.let { + if (it.isHeld) { + it.release() + } + } + cancelWatcherJob() + stopSelf() + } - private fun launchWatcherNotification( - description: String = getString(R.string.watcher_notification_text_active) - ) { - val notification = - notificationService.createNotification( - channelId = getString(R.string.watcher_channel_id), - channelName = getString(R.string.watcher_channel_name), - title = getString(R.string.auto_tunnel_title), - description = description, - ) - ServiceCompat.startForeground( - this, - foregroundId, - notification, - Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID, - ) - } + private fun launchWatcherNotification(description: String = getString(R.string.watcher_notification_text_active)) { + val notification = + notificationService.createNotification( + channelId = getString(R.string.watcher_channel_id), + channelName = getString(R.string.watcher_channel_name), + title = getString(R.string.auto_tunnel_title), + description = description, + ) + ServiceCompat.startForeground( + this, + foregroundId, + notification, + Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID, + ) + } - private fun launchWatcherPausedNotification() { - launchWatcherNotification(getString(R.string.watcher_notification_text_paused)) - } + private fun launchWatcherPausedNotification() { + launchWatcherNotification(getString(R.string.watcher_notification_text_paused)) + } - private fun initWakeLock() { - wakeLock = - (getSystemService(Context.POWER_SERVICE) as PowerManager).run { - 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(Context.POWER_SERVICE) as PowerManager).run { + 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 cancelWatcherJob() { - try { - watcherJob?.cancel() - } catch (e: CancellationException) { - Timber.i("Watcher job cancelled") - } - } + private fun cancelWatcherJob() { + try { + watcherJob?.cancel() + } catch (e: CancellationException) { + Timber.i("Watcher job cancelled") + } + } - private fun startWatcherJob() { - watcherJob = - lifecycleScope.launch { - val setting = appDataRepository.settings.getSettings() - launch { - Timber.i("Starting wifi watcher") - watchForWifiConnectivityChanges() - } - if (setting.isTunnelOnMobileDataEnabled) { - launch { - Timber.i("Starting mobile data watcher") - watchForMobileDataConnectivityChanges() - } - } - if (setting.isTunnelOnEthernetEnabled) { - launch { - Timber.i("Starting ethernet data watcher") - watchForEthernetConnectivityChanges() - } - } - launch { - Timber.i("Starting settings watcher") - watchForSettingsChanges() - } - if (setting.isPingEnabled) { - launch { - Timber.i("Starting ping watcher") - watchForPingFailure() - } - } - launch { - Timber.i("Starting management watcher") - manageVpn() - } + private fun startWatcherJob() { + watcherJob = + lifecycleScope.launch { + val setting = appDataRepository.settings.getSettings() + launch { + Timber.i("Starting wifi watcher") + watchForWifiConnectivityChanges() + } + if (setting.isTunnelOnMobileDataEnabled) { + launch { + Timber.i("Starting mobile data watcher") + watchForMobileDataConnectivityChanges() + } + } + if (setting.isTunnelOnEthernetEnabled) { + launch { + Timber.i("Starting ethernet data watcher") + watchForEthernetConnectivityChanges() + } + } + launch { + Timber.i("Starting settings watcher") + watchForSettingsChanges() + } + if (setting.isPingEnabled) { + launch { + Timber.i("Starting ping watcher") + watchForPingFailure() + } + } + launch { + Timber.i("Starting management watcher") + manageVpn() + } + } + } - } - } + private suspend fun watchForMobileDataConnectivityChanges() { + withContext(ioDispatcher) { + mobileDataService.networkStatus.collect { status -> + when (status) { + is NetworkStatus.Available -> { + Timber.i("Gained Mobile data connection") + networkEventsFlow.update { + it.copy( + isMobileDataConnected = true, + ) + } + } - private suspend fun watchForMobileDataConnectivityChanges() { - withContext(ioDispatcher) { - mobileDataService.networkStatus.collect { status -> - when (status) { - is NetworkStatus.Available -> { - Timber.i("Gained Mobile data connection") - networkEventsFlow.update { - it.copy( - isMobileDataConnected = true, - ) - } - } + is NetworkStatus.CapabilitiesChanged -> { + networkEventsFlow.update { + it.copy( + isMobileDataConnected = true, + ) + } + Timber.i("Mobile data capabilities changed") + } - is NetworkStatus.CapabilitiesChanged -> { - networkEventsFlow.update { - it.copy( - isMobileDataConnected = true, - ) - } - Timber.i("Mobile data capabilities changed") - } + is NetworkStatus.Unavailable -> { + networkEventsFlow.update { + it.copy( + isMobileDataConnected = false, + ) + } + Timber.i("Lost mobile data connection") + } + } + } + } + } - is NetworkStatus.Unavailable -> { - networkEventsFlow.update { - it.copy( - isMobileDataConnected = false, - ) - } - Timber.i("Lost mobile data connection") - } - } - } - } - } + private suspend fun watchForPingFailure() { + val context = this + withContext(ioDispatcher) { + try { + do { + if (vpnService.vpnState.value.status == TunnelState.UP) { + val tunnelConfig = vpnService.vpnState.value.tunnelConfig + tunnelConfig?.let { + val config = TunnelConfig.configFromWgQuick(it.wgQuick) + val results = + config.peers.map { peer -> + val host = + if (peer.endpoint.isPresent && + peer.endpoint.get().resolved.isPresent + ) { + peer.endpoint.get().resolved.get().host + } else { + Constants.DEFAULT_PING_IP + } + Timber.i("Checking reachability of: $host") + val reachable = + InetAddress.getByName(host) + .isReachable(Constants.PING_TIMEOUT.toInt()) + Timber.i("Result: reachable - $reachable") + reachable + } + if (results.contains(false)) { + Timber.i("Restarting VPN for ping failure") + serviceManager.stopVpnServiceForeground(context) + delay(Constants.VPN_RESTART_DELAY) + serviceManager.startVpnServiceForeground(context, it.id) + delay(Constants.PING_COOLDOWN) + } + } + } + delay(Constants.PING_INTERVAL) + } while (true) + } catch (e: Exception) { + Timber.e(e) + } + } + } - private suspend fun watchForPingFailure() { - val context = this - withContext(ioDispatcher) { - try { - do { - if (vpnService.vpnState.value.status == TunnelState.UP) { - val tunnelConfig = vpnService.vpnState.value.tunnelConfig - tunnelConfig?.let { - val config = TunnelConfig.configFromWgQuick(it.wgQuick) - val results = config.peers.map { peer -> - val host = if (peer.endpoint.isPresent && - peer.endpoint.get().resolved.isPresent) - peer.endpoint.get().resolved.get().host - else Constants.DEFAULT_PING_IP - Timber.i("Checking reachability of: $host") - val reachable = InetAddress.getByName(host) - .isReachable(Constants.PING_TIMEOUT.toInt()) - Timber.i("Result: reachable - $reachable") - reachable - } - if (results.contains(false)) { - Timber.i("Restarting VPN for ping failure") - serviceManager.stopVpnServiceForeground(context) - delay(Constants.VPN_RESTART_DELAY) - serviceManager.startVpnServiceForeground(context, it.id) - delay(Constants.PING_COOLDOWN) - } - } - } - delay(Constants.PING_INTERVAL) - } while (true) - } catch (e: Exception) { - Timber.e(e) - } - } - } + private suspend fun watchForSettingsChanges() { + appDataRepository.settings.getSettingsFlow().collect { settings -> + if (networkEventsFlow.value.settings.isAutoTunnelPaused + != settings.isAutoTunnelPaused + ) { + when (settings.isAutoTunnelPaused) { + true -> launchWatcherPausedNotification() + false -> launchWatcherNotification() + } + } + networkEventsFlow.update { + it.copy( + settings = settings, + ) + } + } + } - private suspend fun watchForSettingsChanges() { - appDataRepository.settings.getSettingsFlow().collect { settings -> - if (networkEventsFlow.value.settings.isAutoTunnelPaused != settings.isAutoTunnelPaused) { - when (settings.isAutoTunnelPaused) { - true -> launchWatcherPausedNotification() - false -> launchWatcherNotification() - } - } - networkEventsFlow.update { - it.copy( - settings = settings, - ) - } - } - } + private suspend fun watchForEthernetConnectivityChanges() { + withContext(ioDispatcher) { + ethernetService.networkStatus.collect { status -> + when (status) { + is NetworkStatus.Available -> { + Timber.i("Gained Ethernet connection") + networkEventsFlow.update { + it.copy( + isEthernetConnected = true, + ) + } + } - private suspend fun watchForEthernetConnectivityChanges() { - withContext(ioDispatcher) { - ethernetService.networkStatus.collect { status -> - when (status) { - is NetworkStatus.Available -> { - Timber.i("Gained Ethernet connection") - networkEventsFlow.update { - it.copy( - isEthernetConnected = true, - ) - } - } + is NetworkStatus.CapabilitiesChanged -> { + Timber.i("Ethernet capabilities changed") + networkEventsFlow.update { + it.copy( + isEthernetConnected = true, + ) + } + } - is NetworkStatus.CapabilitiesChanged -> { - Timber.i("Ethernet capabilities changed") - networkEventsFlow.update { - it.copy( - isEthernetConnected = true, - ) - } - } + is NetworkStatus.Unavailable -> { + networkEventsFlow.update { + it.copy( + isEthernetConnected = false, + ) + } + Timber.i("Lost Ethernet connection") + } + } + } + } + } - is NetworkStatus.Unavailable -> { - networkEventsFlow.update { - it.copy( - isEthernetConnected = false, - ) - } - Timber.i("Lost Ethernet connection") - } - } - } - } - } + private suspend fun watchForWifiConnectivityChanges() { + withContext(ioDispatcher) { + wifiService.networkStatus.collect { status -> + when (status) { + is NetworkStatus.Available -> { + Timber.i("Gained Wi-Fi connection") + networkEventsFlow.update { + it.copy( + isWifiConnected = true, + ) + } + } - private suspend fun watchForWifiConnectivityChanges() { - withContext(ioDispatcher) { - wifiService.networkStatus.collect { status -> - when (status) { - is NetworkStatus.Available -> { - Timber.i("Gained Wi-Fi connection") - networkEventsFlow.update { - it.copy( - isWifiConnected = true, - ) - } - } + is NetworkStatus.CapabilitiesChanged -> { + Timber.i("Wifi capabilities changed") + networkEventsFlow.update { + it.copy( + isWifiConnected = true, + ) + } + val ssid = wifiService.getNetworkName(status.networkCapabilities) + ssid?.let { name -> + if (name.contains(Constants.UNREADABLE_SSID)) { + Timber.w("SSID unreadable: missing permissions") + } else { + Timber.i("Detected valid SSID") + } + appDataRepository.appState.setCurrentSsid(name) + networkEventsFlow.update { + it.copy( + currentNetworkSSID = name, + ) + } + } ?: Timber.w("Failed to read ssid") + } - is NetworkStatus.CapabilitiesChanged -> { - Timber.i("Wifi capabilities changed") - networkEventsFlow.update { - it.copy( - isWifiConnected = true, - ) - } - val ssid = wifiService.getNetworkName(status.networkCapabilities) - ssid?.let { name -> - if (name.contains(Constants.UNREADABLE_SSID)) { - Timber.w("SSID unreadable: missing permissions") - } else Timber.i("Detected valid SSID") - appDataRepository.appState.setCurrentSsid(name) - networkEventsFlow.update { - it.copy( - currentNetworkSSID = name, - ) - } - } ?: Timber.w("Failed to read ssid") - } + is NetworkStatus.Unavailable -> { + networkEventsFlow.update { + it.copy( + isWifiConnected = false, + ) + } + Timber.i("Lost Wi-Fi connection") + } + } + } + } + } - is NetworkStatus.Unavailable -> { - networkEventsFlow.update { - it.copy( - isWifiConnected = false, - ) - } - Timber.i("Lost Wi-Fi connection") - } - } - } - } - } + private suspend fun getMobileDataTunnel(): TunnelConfig? { + return appDataRepository.tunnels.findByMobileDataTunnel().firstOrNull() + } - private suspend fun getMobileDataTunnel(): TunnelConfig? { - return appDataRepository.tunnels.findByMobileDataTunnel().firstOrNull() - } + private suspend fun getSsidTunnel(ssid: String): TunnelConfig? { + return appDataRepository.tunnels.findByTunnelNetworksName(ssid).firstOrNull() + } - private suspend fun getSsidTunnel(ssid: String): TunnelConfig? { - return appDataRepository.tunnels.findByTunnelNetworksName(ssid).firstOrNull() - } + private fun isTunnelDown(): Boolean { + return vpnService.vpnState.value.status == TunnelState.DOWN + } - private fun isTunnelDown(): Boolean { - return vpnService.vpnState.value.status == TunnelState.DOWN - } + private suspend fun manageVpn() { + val context = this + withContext(ioDispatcher) { + networkEventsFlow.collectLatest { watcherState -> + val autoTunnel = "Auto-tunnel watcher" + if (!watcherState.settings.isAutoTunnelPaused) { + // delay for rapid network state changes and then collect latest + delay(Constants.WATCHER_COLLECTION_DELAY) + val tunnelConfig = vpnService.vpnState.value.tunnelConfig + when { + watcherState.isEthernetConditionMet() -> { + Timber.i("$autoTunnel - tunnel on on ethernet condition met") + if (isTunnelDown()) serviceManager.startVpnServiceForeground(context) + } - private suspend fun manageVpn() { - val context = this - withContext(ioDispatcher) { - networkEventsFlow.collectLatest { watcherState -> - val autoTunnel = "Auto-tunnel watcher" - if (!watcherState.settings.isAutoTunnelPaused) { - //delay for rapid network state changes and then collect latest - delay(Constants.WATCHER_COLLECTION_DELAY) - val tunnelConfig = vpnService.vpnState.value.tunnelConfig - when { - watcherState.isEthernetConditionMet() -> { - Timber.i("$autoTunnel - tunnel on on ethernet condition met") - if (isTunnelDown()) serviceManager.startVpnServiceForeground(context) - } + watcherState.isMobileDataConditionMet() -> { + Timber.i("$autoTunnel - tunnel on mobile data condition met") + val mobileDataTunnel = getMobileDataTunnel() + val tunnel = + mobileDataTunnel ?: appDataRepository.getPrimaryOrFirstTunnel() + if (isTunnelDown() || tunnelConfig?.isMobileDataTunnel == false) { + serviceManager.startVpnServiceForeground( + context, + tunnel?.id, + ) + } + } - watcherState.isMobileDataConditionMet() -> { - Timber.i("$autoTunnel - tunnel on mobile data condition met") - val mobileDataTunnel = getMobileDataTunnel() - val tunnel = - mobileDataTunnel ?: appDataRepository.getPrimaryOrFirstTunnel() - if (isTunnelDown() || tunnelConfig?.isMobileDataTunnel == false) { - serviceManager.startVpnServiceForeground( - context, - tunnel?.id, - ) - } - } + watcherState.isTunnelOffOnMobileDataConditionMet() -> { + Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off") + if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context) + } - watcherState.isTunnelOffOnMobileDataConditionMet() -> { - Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off") - if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context) - } + watcherState.isUntrustedWifiConditionMet() -> { + if (tunnelConfig?.tunnelNetworks?.contains(watcherState.currentNetworkSSID) == false || + tunnelConfig == null + ) { + Timber.i( + "$autoTunnel - tunnel on ssid not associated with current tunnel condition met", + ) + getSsidTunnel(watcherState.currentNetworkSSID)?.let { + Timber.i("Found tunnel associated with this SSID, bringing tunnel up: ${it.name}") + if (isTunnelDown() || tunnelConfig?.id != it.id) { + serviceManager.startVpnServiceForeground( + context, + it.id, + ) + } + } ?: suspend { + Timber.i("No tunnel associated with this SSID, using defaults") + val default = appDataRepository.getPrimaryOrFirstTunnel() + if (default?.name != vpnService.name) { + default?.let { + serviceManager.startVpnServiceForeground(context, it.id) + } + } + }.invoke() + } + } - watcherState.isUntrustedWifiConditionMet() -> { - if (tunnelConfig?.tunnelNetworks?.contains(watcherState.currentNetworkSSID) == false || - tunnelConfig == null) { - Timber.i("$autoTunnel - tunnel on ssid not associated with current tunnel condition met") - getSsidTunnel(watcherState.currentNetworkSSID)?.let { - Timber.i("Found tunnel associated with this SSID, bringing tunnel up: ${it.name}") - if (isTunnelDown() || tunnelConfig?.id != it.id) serviceManager.startVpnServiceForeground( - context, - it.id, - ) - } ?: suspend { - Timber.i("No tunnel associated with this SSID, using defaults") - val default = appDataRepository.getPrimaryOrFirstTunnel() - if (default?.name != vpnService.name) { - default?.let { - serviceManager.startVpnServiceForeground(context, it.id) - } + watcherState.isTrustedWifiConditionMet() -> { + Timber.i( + "$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off", + ) + if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context) + } - } - }.invoke() - } - } + watcherState.isTunnelOffOnWifiConditionMet() -> { + Timber.i( + "$autoTunnel - tunnel off on wifi condition met, turning vpn off", + ) + if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context) + } - watcherState.isTrustedWifiConditionMet() -> { - Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off") - if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context) - } + watcherState.isTunnelOffOnNoConnectivityMet() -> { + Timber.i( + "$autoTunnel - tunnel off on no connectivity met, turning vpn off", + ) + if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context) + } - watcherState.isTunnelOffOnWifiConditionMet() -> { - Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off") - if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context) - } - - watcherState.isTunnelOffOnNoConnectivityMet() -> { - Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off") - if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context) - } - - else -> { - Timber.i("$autoTunnel - no condition met") - } - } - } - } - } - } + else -> { + Timber.i("$autoTunnel - no condition met") + } + } + } + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt index 5ff3d8c8..cf3667a6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt @@ -29,173 +29,170 @@ import javax.inject.Inject @AndroidEntryPoint class WireGuardTunnelService : ForegroundService() { - private val foregroundId = 123 + private val foregroundId = 123 - @Inject - lateinit var vpnService: VpnService + @Inject + lateinit var vpnService: VpnService - @Inject - lateinit var appDataRepository: AppDataRepository + @Inject + lateinit var appDataRepository: AppDataRepository - @Inject - lateinit var notificationService: NotificationService + @Inject + lateinit var notificationService: NotificationService - @Inject - @MainImmediateDispatcher - lateinit var mainImmediateDispatcher: CoroutineDispatcher + @Inject + @MainImmediateDispatcher + lateinit var mainImmediateDispatcher: CoroutineDispatcher - @Inject - @IoDispatcher - lateinit var ioDispatcher: CoroutineDispatcher + @Inject + @IoDispatcher + lateinit var ioDispatcher: CoroutineDispatcher - private var job: Job? = null + private var job: Job? = null - private var didShowConnected = false + private var didShowConnected = false - override fun onCreate() { - super.onCreate() - lifecycleScope.launch(mainImmediateDispatcher) { - //TODO fix this to not launch if AOVPN - if (appDataRepository.tunnels.count() != 0) { - launchVpnNotification() - } - } - } + override fun onCreate() { + super.onCreate() + lifecycleScope.launch(mainImmediateDispatcher) { + // TODO fix this to not launch if AOVPN + if (appDataRepository.tunnels.count() != 0) { + launchVpnNotification() + } + } + } - override fun startService(extras: Bundle?) { - super.startService(extras) - cancelJob() - job = - lifecycleScope.launch { - launch { - val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY) - if (vpnService.getState() == TunnelState.UP) { - vpnService.stopTunnel() - } - vpnService.startTunnel( - tunnelId?.let { - appDataRepository.tunnels.getById(it) - }, - ) - } - launch { - handshakeNotifications() - } - } - } + override fun startService(extras: Bundle?) { + super.startService(extras) + cancelJob() + job = + lifecycleScope.launch { + launch { + val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY) + if (vpnService.getState() == TunnelState.UP) { + vpnService.stopTunnel() + } + vpnService.startTunnel( + tunnelId?.let { + appDataRepository.tunnels.getById(it) + }, + ) + } + launch { + handshakeNotifications() + } + } + } - //TODO improve tunnel notifications - private suspend fun handshakeNotifications() { - withContext(ioDispatcher) { - var tunnelName: String? = null - vpnService.vpnState.collect { state -> - state.statistics - ?.mapPeerStats() - ?.map { it.value?.handshakeStatus() } - .let { statuses -> - when { - statuses?.all { it == HandshakeStatus.HEALTHY } == true -> { - if (!didShowConnected) { - delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY) - tunnelName = state.tunnelConfig?.name - launchVpnNotification( - getString(R.string.tunnel_start_title), - "${getString(R.string.tunnel_start_text)} - $tunnelName", - ) - didShowConnected = true - } - } + // TODO improve tunnel notifications + private suspend fun handshakeNotifications() { + withContext(ioDispatcher) { + var tunnelName: String? = null + vpnService.vpnState.collect { state -> + state.statistics + ?.mapPeerStats() + ?.map { it.value?.handshakeStatus() } + .let { statuses -> + when { + statuses?.all { it == HandshakeStatus.HEALTHY } == true -> { + if (!didShowConnected) { + delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY) + tunnelName = state.tunnelConfig?.name + launchVpnNotification( + getString(R.string.tunnel_start_title), + "${getString(R.string.tunnel_start_text)} - $tunnelName", + ) + didShowConnected = true + } + } - statuses?.any { it == HandshakeStatus.STALE } == true -> {} - statuses?.all { it == HandshakeStatus.NOT_STARTED } == - true -> { - } + statuses?.any { it == HandshakeStatus.STALE } == true -> {} + statuses?.all { it == HandshakeStatus.NOT_STARTED } == + true -> { + } - else -> {} - } - } - if (state.status == TunnelState.UP && state.tunnelConfig?.name != tunnelName) { - tunnelName = state.tunnelConfig?.name - launchVpnNotification( - getString(R.string.tunnel_start_title), - "${getString(R.string.tunnel_start_text)} - $tunnelName", - ) - } - } - } - } + else -> {} + } + } + if (state.status == TunnelState.UP && state.tunnelConfig?.name != tunnelName) { + tunnelName = state.tunnelConfig?.name + launchVpnNotification( + getString(R.string.tunnel_start_title), + "${getString(R.string.tunnel_start_text)} - $tunnelName", + ) + } + } + } + } - private fun launchAlwaysOnDisabledNotification() { - launchVpnNotification( - title = this.getString(R.string.vpn_connection_failed), - description = this.getString(R.string.always_on_disabled), - ) - } + private fun launchAlwaysOnDisabledNotification() { + launchVpnNotification( + title = this.getString(R.string.vpn_connection_failed), + description = this.getString(R.string.always_on_disabled), + ) + } - override fun stopService() { - super.stopService() - lifecycleScope.launch { - vpnService.stopTunnel() - didShowConnected = false - } - cancelJob() - stopSelf() - } + override fun stopService() { + super.stopService() + lifecycleScope.launch { + vpnService.stopTunnel() + didShowConnected = false + } + cancelJob() + stopSelf() + } - private fun launchVpnNotification( - title: String = getString(R.string.vpn_starting), - description: String = getString(R.string.attempt_connection) - ) { - val notification = - notificationService.createNotification( - channelId = getString(R.string.vpn_channel_id), - channelName = getString(R.string.vpn_channel_name), - title = title, - onGoing = false, - vibration = false, - showTimestamp = true, - description = description, - ) - ServiceCompat.startForeground( - this, - foregroundId, - notification, - Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID, - ) - } + private fun launchVpnNotification(title: String = getString(R.string.vpn_starting), description: String = getString(R.string.attempt_connection)) { + val notification = + notificationService.createNotification( + channelId = getString(R.string.vpn_channel_id), + channelName = getString(R.string.vpn_channel_name), + title = title, + onGoing = false, + vibration = false, + showTimestamp = true, + description = description, + ) + ServiceCompat.startForeground( + this, + foregroundId, + notification, + Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID, + ) + } - private fun launchVpnConnectionFailedNotification(message: String) { - val notification = - notificationService.createNotification( - channelId = getString(R.string.vpn_channel_id), - channelName = getString(R.string.vpn_channel_name), - action = - PendingIntent.getBroadcast( - this, - 0, - Intent(this, NotificationActionReceiver::class.java), - PendingIntent.FLAG_IMMUTABLE, - ), - actionText = getString(R.string.restart), - title = getString(R.string.vpn_connection_failed), - onGoing = false, - vibration = true, - showTimestamp = true, - description = message, - ) - ServiceCompat.startForeground( - this, - foregroundId, - notification, - Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID, - ) - } + private fun launchVpnConnectionFailedNotification(message: String) { + val notification = + notificationService.createNotification( + channelId = getString(R.string.vpn_channel_id), + channelName = getString(R.string.vpn_channel_name), + action = + PendingIntent.getBroadcast( + this, + 0, + Intent(this, NotificationActionReceiver::class.java), + PendingIntent.FLAG_IMMUTABLE, + ), + actionText = getString(R.string.restart), + title = getString(R.string.vpn_connection_failed), + onGoing = false, + vibration = true, + showTimestamp = true, + description = message, + ) + ServiceCompat.startForeground( + this, + foregroundId, + notification, + Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID, + ) + } - private fun cancelJob() { - try { - job?.cancel() - } catch (e: CancellationException) { - Timber.i("Tunnel job cancelled") - } - } + private fun cancelJob() { + try { + job?.cancel() + } catch (e: CancellationException) { + Timber.i("Tunnel job cancelled") + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/BaseNetworkService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/BaseNetworkService.kt index 7bbff4d4..e39e11d1 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/BaseNetworkService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/BaseNetworkService.kt @@ -15,118 +15,113 @@ import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.map abstract class BaseNetworkService>( - val context: Context, - networkCapability: Int + val context: Context, + networkCapability: Int, ) : NetworkService { - private val connectivityManager = - context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + private val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - private val wifiManager = - context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + private val wifiManager = + context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager - override val networkStatus = callbackFlow { - val networkStatusCallback = - when (Build.VERSION.SDK_INT) { - in Build.VERSION_CODES.S..Int.MAX_VALUE -> { - object : - ConnectivityManager.NetworkCallback( - FLAG_INCLUDE_LOCATION_INFO, - ) { - override fun onAvailable(network: Network) { - trySend(NetworkStatus.Available(network)) - } + override val networkStatus = + callbackFlow { + val networkStatusCallback = + when (Build.VERSION.SDK_INT) { + in Build.VERSION_CODES.S..Int.MAX_VALUE -> { + object : + ConnectivityManager.NetworkCallback( + FLAG_INCLUDE_LOCATION_INFO, + ) { + override fun onAvailable(network: Network) { + trySend(NetworkStatus.Available(network)) + } - override fun onLost(network: Network) { - trySend(NetworkStatus.Unavailable(network)) - } + override fun onLost(network: Network) { + trySend(NetworkStatus.Unavailable(network)) + } - override fun onCapabilitiesChanged( - network: Network, - networkCapabilities: NetworkCapabilities - ) { - trySend( - NetworkStatus.CapabilitiesChanged( - network, - networkCapabilities, - ), - ) - } - } - } + override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { + trySend( + NetworkStatus.CapabilitiesChanged( + network, + networkCapabilities, + ), + ) + } + } + } - else -> { - object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - trySend(NetworkStatus.Available(network)) - } + else -> { + object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + trySend(NetworkStatus.Available(network)) + } - override fun onLost(network: Network) { - trySend(NetworkStatus.Unavailable(network)) - } + override fun onLost(network: Network) { + trySend(NetworkStatus.Unavailable(network)) + } - override fun onCapabilitiesChanged( - network: Network, - networkCapabilities: NetworkCapabilities - ) { - trySend( - NetworkStatus.CapabilitiesChanged( - network, - networkCapabilities, - ), - ) - } - } - } - } - val request = - NetworkRequest.Builder() - .addTransportType(networkCapability) - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) - .build() - connectivityManager.registerNetworkCallback(request, networkStatusCallback) + override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { + trySend( + NetworkStatus.CapabilitiesChanged( + network, + networkCapabilities, + ), + ) + } + } + } + } + val request = + NetworkRequest.Builder() + .addTransportType(networkCapability) + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + .build() + connectivityManager.registerNetworkCallback(request, networkStatusCallback) - awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) } - } + awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) } + } - override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? { - var ssid: String? = getWifiNameFromCapabilities(networkCapabilities) - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { - val info = wifiManager.connectionInfo - if (info.supplicantState === SupplicantState.COMPLETED) { - ssid = info.ssid - } - } - return ssid?.trim('"') - } + override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? { + var ssid: String? = getWifiNameFromCapabilities(networkCapabilities) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + val info = wifiManager.connectionInfo + if (info.supplicantState === SupplicantState.COMPLETED) { + ssid = info.ssid + } + } + return ssid?.trim('"') + } - companion object { - private fun getWifiNameFromCapabilities(networkCapabilities: NetworkCapabilities): String? { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - val info: WifiInfo - if (networkCapabilities.transportInfo is WifiInfo) { - info = networkCapabilities.transportInfo as WifiInfo - return info.ssid - } - } - return null - } - } + companion object { + private fun getWifiNameFromCapabilities(networkCapabilities: NetworkCapabilities): String? { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val info: WifiInfo + if (networkCapabilities.transportInfo is WifiInfo) { + info = networkCapabilities.transportInfo as WifiInfo + return info.ssid + } + } + return null + } + } } inline fun Flow.map( - crossinline onUnavailable: suspend (network: Network) -> Result, - crossinline onAvailable: suspend (network: Network) -> Result, - crossinline onCapabilitiesChanged: - suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result + crossinline onUnavailable: suspend (network: Network) -> Result, + crossinline onAvailable: suspend (network: Network) -> Result, + crossinline onCapabilitiesChanged: + suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result, ): Flow = map { status -> - when (status) { - is NetworkStatus.Unavailable -> onUnavailable(status.network) - is NetworkStatus.Available -> onAvailable(status.network) - is NetworkStatus.CapabilitiesChanged -> - onCapabilitiesChanged( - status.network, - status.networkCapabilities, - ) - } + when (status) { + is NetworkStatus.Unavailable -> onUnavailable(status.network) + is NetworkStatus.Available -> onAvailable(status.network) + is NetworkStatus.CapabilitiesChanged -> + onCapabilitiesChanged( + status.network, + status.networkCapabilities, + ) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/EthernetService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/EthernetService.kt index 36f76eb7..24a8b5de 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/EthernetService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/EthernetService.kt @@ -5,5 +5,9 @@ import android.net.NetworkCapabilities import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject -class EthernetService @Inject constructor(@ApplicationContext context: Context) : - BaseNetworkService(context, NetworkCapabilities.TRANSPORT_ETHERNET) +class EthernetService +@Inject +constructor( + @ApplicationContext context: Context, +) : + BaseNetworkService(context, NetworkCapabilities.TRANSPORT_ETHERNET) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/MobileDataService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/MobileDataService.kt index 626ca4e0..034ba9fa 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/MobileDataService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/MobileDataService.kt @@ -5,5 +5,9 @@ import android.net.NetworkCapabilities import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject -class MobileDataService @Inject constructor(@ApplicationContext context: Context) : - BaseNetworkService(context, NetworkCapabilities.TRANSPORT_CELLULAR) +class MobileDataService +@Inject +constructor( + @ApplicationContext context: Context, +) : + BaseNetworkService(context, NetworkCapabilities.TRANSPORT_CELLULAR) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/NetworkService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/NetworkService.kt index 88c9416f..609164a3 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/NetworkService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/NetworkService.kt @@ -4,7 +4,7 @@ import android.net.NetworkCapabilities import kotlinx.coroutines.flow.Flow interface NetworkService { - fun getNetworkName(networkCapabilities: NetworkCapabilities): String? + fun getNetworkName(networkCapabilities: NetworkCapabilities): String? - val networkStatus: Flow + val networkStatus: Flow } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/NetworkStatus.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/NetworkStatus.kt index 63365a0a..99930af3 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/NetworkStatus.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/NetworkStatus.kt @@ -4,10 +4,10 @@ import android.net.Network import android.net.NetworkCapabilities sealed class NetworkStatus { - class Available(val network: Network) : NetworkStatus() + class Available(val network: Network) : NetworkStatus() - class Unavailable(val network: Network) : NetworkStatus() + class Unavailable(val network: Network) : NetworkStatus() - class CapabilitiesChanged(val network: Network, val networkCapabilities: NetworkCapabilities) : - NetworkStatus() + class CapabilitiesChanged(val network: Network, val networkCapabilities: NetworkCapabilities) : + NetworkStatus() } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/WifiService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/WifiService.kt index aafccb4f..8bba30a7 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/WifiService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/WifiService.kt @@ -5,5 +5,9 @@ import android.net.NetworkCapabilities import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject -class WifiService @Inject constructor(@ApplicationContext context: Context) : - BaseNetworkService(context, NetworkCapabilities.TRANSPORT_WIFI) +class WifiService +@Inject +constructor( + @ApplicationContext context: Context, +) : + BaseNetworkService(context, NetworkCapabilities.TRANSPORT_WIFI) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/NotificationService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/NotificationService.kt index cabd17b1..0cc604d8 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/NotificationService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/NotificationService.kt @@ -5,18 +5,18 @@ import android.app.NotificationManager import android.app.PendingIntent interface NotificationService { - fun createNotification( - channelId: String, - channelName: String, - title: String = "", - action: PendingIntent? = null, - actionText: String? = null, - description: String, - showTimestamp: Boolean = false, - importance: Int = NotificationManager.IMPORTANCE_HIGH, - vibration: Boolean = false, - onGoing: Boolean = true, - lights: Boolean = true, - onlyAlertOnce: Boolean = true, - ): Notification + fun createNotification( + channelId: String, + channelName: String, + title: String = "", + action: PendingIntent? = null, + actionText: String? = null, + description: String, + showTimestamp: Boolean = false, + importance: Int = NotificationManager.IMPORTANCE_HIGH, + vibration: Boolean = false, + onGoing: Boolean = true, + lights: Boolean = true, + onlyAlertOnce: Boolean = true, + ): Notification } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/WireGuardNotification.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/WireGuardNotification.kt index 78dc9a6c..c8c0d88b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/WireGuardNotification.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/WireGuardNotification.kt @@ -13,89 +13,93 @@ import com.zaneschepke.wireguardautotunnel.ui.SplashActivity import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject -class WireGuardNotification @Inject constructor(@ApplicationContext private val context: Context) : - NotificationService { - private val notificationManager = - context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager +class WireGuardNotification +@Inject +constructor( + @ApplicationContext private val context: Context, +) : + NotificationService { + private val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - private val watcherBuilder: NotificationCompat.Builder = - NotificationCompat.Builder( - context, - context.getString(R.string.watcher_channel_id), - ) - private val tunnelBuilder: NotificationCompat.Builder = - NotificationCompat.Builder( - context, - context.getString(R.string.vpn_channel_id), - ) + private val watcherBuilder: NotificationCompat.Builder = + NotificationCompat.Builder( + context, + context.getString(R.string.watcher_channel_id), + ) + private val tunnelBuilder: NotificationCompat.Builder = + NotificationCompat.Builder( + context, + context.getString(R.string.vpn_channel_id), + ) - override fun createNotification( - channelId: String, - channelName: String, - title: String, - action: PendingIntent?, - actionText: String?, - description: String, - showTimestamp: Boolean, - importance: Int, - vibration: Boolean, - onGoing: Boolean, - lights: Boolean, - onlyAlertOnce: Boolean, - ): Notification { - val channel = - NotificationChannel( - channelId, - channelName, - importance, - ) - .let { - it.description = title - it.enableLights(lights) - it.lightColor = Color.RED - it.enableVibration(vibration) - it.vibrationPattern = longArrayOf(100, 200, 300) - it - } - notificationManager.createNotificationChannel(channel) - val pendingIntent: PendingIntent = - Intent(context, SplashActivity::class.java).let { notificationIntent -> - PendingIntent.getActivity( - context, - 0, - notificationIntent, - PendingIntent.FLAG_IMMUTABLE, - ) - } + override fun createNotification( + channelId: String, + channelName: String, + title: String, + action: PendingIntent?, + actionText: String?, + description: String, + showTimestamp: Boolean, + importance: Int, + vibration: Boolean, + onGoing: Boolean, + lights: Boolean, + onlyAlertOnce: Boolean, + ): Notification { + val channel = + NotificationChannel( + channelId, + channelName, + importance, + ) + .let { + it.description = title + it.enableLights(lights) + it.lightColor = Color.RED + it.enableVibration(vibration) + it.vibrationPattern = longArrayOf(100, 200, 300) + it + } + notificationManager.createNotificationChannel(channel) + val pendingIntent: PendingIntent = + Intent(context, SplashActivity::class.java).let { notificationIntent -> + PendingIntent.getActivity( + context, + 0, + notificationIntent, + PendingIntent.FLAG_IMMUTABLE, + ) + } - val builder = - when (channelId) { - context.getString(R.string.watcher_channel_id) -> watcherBuilder - context.getString(R.string.vpn_channel_id) -> tunnelBuilder - else -> { - NotificationCompat.Builder( - context, - channelId, - ) - } - } + val builder = + when (channelId) { + context.getString(R.string.watcher_channel_id) -> watcherBuilder + context.getString(R.string.vpn_channel_id) -> tunnelBuilder + else -> { + NotificationCompat.Builder( + context, + channelId, + ) + } + } - return builder.let { - if (action != null && actionText != null) { - it.addAction( - NotificationCompat.Action.Builder(0, actionText, action).build(), - ) - it.setAutoCancel(true) - } - it.setContentTitle(title) - .setContentText(description) - .setOnlyAlertOnce(onlyAlertOnce) - .setContentIntent(pendingIntent) - .setOngoing(onGoing) - .setPriority(NotificationCompat.PRIORITY_HIGH) - .setShowWhen(showTimestamp) - .setSmallIcon(R.drawable.ic_launcher) - .build() - } - } + return builder.let { + if (action != null && actionText != null) { + it.addAction( + NotificationCompat.Action.Builder(0, actionText, action).build(), + ) + it.setAutoCancel(true) + } + it.setContentTitle(title) + .setContentText(description) + .setOnlyAlertOnce(onlyAlertOnce) + .setContentIntent(pendingIntent) + .setOngoing(onGoing) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setShowWhen(showTimestamp) + .setSmallIcon(R.drawable.ic_launcher) + .build() + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt index c4f0f055..114bcfed 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt @@ -15,65 +15,71 @@ 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 + @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.getSettings() + if (settings.isShortcutsEnabled) { + when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) { + WireGuardTunnelService::class.java.simpleName -> { + val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY) + val tunnelConfig = + tunnelName?.let { + appDataRepository.tunnels.getAll().firstOrNull { + it.name == tunnelName + } + } + when (intent.action) { + Action.START.name -> + serviceManager.startVpnServiceForeground( + this@ShortcutsActivity, + tunnelConfig?.id, + isManualStart = true, + ) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - applicationScope.launch { - val settings = appDataRepository.settings.getSettings() - if (settings.isShortcutsEnabled) { - when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) { - WireGuardTunnelService::class.java.simpleName -> { - val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY) - val tunnelConfig = tunnelName?.let { - appDataRepository.tunnels.getAll().firstOrNull { - it.name == tunnelName - } - } - when (intent.action) { - Action.START.name -> serviceManager.startVpnServiceForeground( - this@ShortcutsActivity, tunnelConfig?.id, isManualStart = true, - ) + Action.STOP.name -> + serviceManager.stopVpnServiceForeground( + this@ShortcutsActivity, + isManualStop = true, + ) + } + } - Action.STOP.name -> serviceManager.stopVpnServiceForeground( - this@ShortcutsActivity, - isManualStop = true, - ) - } - } + WireGuardConnectivityWatcherService::class.java.simpleName -> { + when (intent.action) { + Action.START.name -> + appDataRepository.settings.save( + settings.copy( + isAutoTunnelPaused = false, + ), + ) - WireGuardConnectivityWatcherService::class.java.simpleName -> { - when (intent.action) { - Action.START.name -> appDataRepository.settings.save( - settings.copy( - isAutoTunnelPaused = false, - ), - ) + Action.STOP.name -> + appDataRepository.settings.save( + settings.copy( + isAutoTunnelPaused = true, + ), + ) + } + } + } + } + } + finish() + } - Action.STOP.name -> appDataRepository.settings.save( - settings.copy( - isAutoTunnelPaused = true, - ), - ) - } - } - } - } - } - finish() - } - - companion object { - const val TUNNEL_NAME_EXTRA_KEY = "tunnelName" - const val CLASS_NAME_EXTRA_KEY = "className" - } + companion object { + const val TUNNEL_NAME_EXTRA_KEY = "tunnelName" + const val CLASS_NAME_EXTRA_KEY = "className" + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/AutoTunnelControlTile.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/AutoTunnelControlTile.kt index c31a1cb8..7da23a6d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/AutoTunnelControlTile.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/AutoTunnelControlTile.kt @@ -19,87 +19,86 @@ 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 dispatcher = ServiceLifecycleDispatcher(this) - private val dispatcher = ServiceLifecycleDispatcher(this) + private var manualStartConfig: TunnelConfig? = null - private var manualStartConfig: TunnelConfig? = null + override fun onStartListening() { + super.onStartListening() + lifecycleScope.launch { + val settings = appDataRepository.settings.getSettings() + when (settings.isAutoTunnelEnabled) { + true -> { + if (settings.isAutoTunnelPaused) { + setInactive() + setTileDescription(this@AutoTunnelControlTile.getString(R.string.paused)) + } else { + setActive() + setTileDescription(this@AutoTunnelControlTile.getString(R.string.active)) + } + } - override fun onStartListening() { - super.onStartListening() - lifecycleScope.launch { - val settings = appDataRepository.settings.getSettings() - when (settings.isAutoTunnelEnabled) { - true -> { - if (settings.isAutoTunnelPaused) { - setInactive() - setTileDescription(this@AutoTunnelControlTile.getString(R.string.paused)) - } else { - setActive() - setTileDescription(this@AutoTunnelControlTile.getString(R.string.active)) - } - } + false -> { + setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled)) + setUnavailable() + } + } + } + } - false -> { - setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled)) - setUnavailable() - } - } - } - } + override fun onTileAdded() { + super.onTileAdded() + onStartListening() + } - override fun onTileAdded() { - super.onTileAdded() - onStartListening() - } + override fun onClick() { + super.onClick() + unlockAndRun { + lifecycleScope.launch { + try { + appDataRepository.toggleWatcherServicePause() + onStartListening() + } catch (e: Exception) { + Timber.e(e.message) + } finally { + cancel() + } + } + } + } - override fun onClick() { - super.onClick() - unlockAndRun { - lifecycleScope.launch { - try { - appDataRepository.toggleWatcherServicePause() - onStartListening() - } catch (e: Exception) { - Timber.e(e.message) - } finally { - cancel() - } - } - } - } + private fun setActive() { + qsTile.state = Tile.STATE_ACTIVE + qsTile.updateTile() + } - private fun setActive() { - qsTile.state = Tile.STATE_ACTIVE - qsTile.updateTile() - } + private fun setInactive() { + qsTile.state = Tile.STATE_INACTIVE + qsTile.updateTile() + } - private fun setInactive() { - qsTile.state = Tile.STATE_INACTIVE - qsTile.updateTile() - } + private fun setUnavailable() { + manualStartConfig = null + qsTile.state = Tile.STATE_UNAVAILABLE + qsTile.updateTile() + } - private fun setUnavailable() { - manualStartConfig = null - qsTile.state = Tile.STATE_UNAVAILABLE - qsTile.updateTile() - } + private fun setTileDescription(description: String) { + 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) { - 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() - } - - override val lifecycle: Lifecycle - get() = dispatcher.lifecycle + override val lifecycle: Lifecycle + get() = dispatcher.lifecycle } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt index de6f1982..d6e38178 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt @@ -20,100 +20,102 @@ import javax.inject.Inject @AndroidEntryPoint class TunnelControlTile : TileService(), LifecycleOwner { + @Inject + lateinit var appDataRepository: AppDataRepository - @Inject - lateinit var appDataRepository: AppDataRepository + @Inject + lateinit var vpnService: VpnService - @Inject - lateinit var vpnService: VpnService + @Inject + lateinit var serviceManager: ServiceManager - @Inject - lateinit var serviceManager: ServiceManager + private val dispatcher = ServiceLifecycleDispatcher(this) - private val dispatcher = ServiceLifecycleDispatcher(this) + private var manualStartConfig: TunnelConfig? = null - private var manualStartConfig: TunnelConfig? = null + override fun onStartListening() { + super.onStartListening() + Timber.d("On start listening called") + lifecycleScope.launch { + when (vpnService.getState()) { + TunnelState.UP -> { + setActive() + setTileDescription(vpnService.name) + } - override fun onStartListening() { - super.onStartListening() - Timber.d("On start listening called") - lifecycleScope.launch { - when (vpnService.getState()) { - TunnelState.UP -> { - setActive() - setTileDescription(vpnService.name) - } + TunnelState.DOWN -> { + setInactive() + val config = + appDataRepository.getStartTunnelConfig()?.also { config -> + manualStartConfig = config + } ?: appDataRepository.getPrimaryOrFirstTunnel() + config?.let { + setTileDescription(it.name) + } ?: setUnavailable() + } - TunnelState.DOWN -> { - setInactive() - val config = appDataRepository.getStartTunnelConfig()?.also { config -> - manualStartConfig = config - } ?: appDataRepository.getPrimaryOrFirstTunnel() - config?.let { - setTileDescription(it.name) - } ?: setUnavailable() - } + else -> setInactive() + } + } + } - else -> setInactive() - } - } - } + override fun onTileAdded() { + super.onTileAdded() + onStartListening() + } - override fun onTileAdded() { - super.onTileAdded() - onStartListening() - } + override fun onClick() { + super.onClick() + unlockAndRun { + lifecycleScope.launch { + try { + if (vpnService.getState() == TunnelState.UP) { + serviceManager.stopVpnServiceForeground( + this@TunnelControlTile, + isManualStop = true, + ) + } else { + serviceManager.startVpnServiceForeground( + this@TunnelControlTile, + manualStartConfig?.id, + isManualStart = true, + ) + } + } catch (e: Exception) { + Timber.e(e.message) + } finally { + cancel() + } + } + } + } - override fun onClick() { - super.onClick() - unlockAndRun { - lifecycleScope.launch { - try { - if (vpnService.getState() == TunnelState.UP) { - serviceManager.stopVpnServiceForeground( - this@TunnelControlTile, - isManualStop = true, - ) - } else { - serviceManager.startVpnServiceForeground( - this@TunnelControlTile, manualStartConfig?.id, isManualStart = true, - ) - } - } catch (e: Exception) { - Timber.e(e.message) - } finally { - cancel() - } - } - } - } + private fun setActive() { + qsTile.state = Tile.STATE_ACTIVE + qsTile.updateTile() + } - private fun setActive() { - qsTile.state = Tile.STATE_ACTIVE - qsTile.updateTile() - } + private fun setInactive() { + qsTile.state = Tile.STATE_INACTIVE + qsTile.updateTile() + } - private fun setInactive() { - qsTile.state = Tile.STATE_INACTIVE - qsTile.updateTile() - } + private fun setUnavailable() { + manualStartConfig = null + qsTile.state = Tile.STATE_UNAVAILABLE + qsTile.updateTile() + } - private fun setUnavailable() { - manualStartConfig = null - qsTile.state = Tile.STATE_UNAVAILABLE - qsTile.updateTile() - } + private fun setTileDescription(description: String) { + 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) { - 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() - } - - override val lifecycle: Lifecycle - get() = dispatcher.lifecycle + override val lifecycle: Lifecycle + get() = dispatcher.lifecycle } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/HandshakeStatus.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/HandshakeStatus.kt index 13bd8a7f..3ceed201 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/HandshakeStatus.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/HandshakeStatus.kt @@ -1,16 +1,17 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel 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/service/tunnel/TunnelState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/TunnelState.kt index 24181ada..af2f738a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/TunnelState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/TunnelState.kt @@ -3,41 +3,42 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel import com.wireguard.android.backend.Tunnel enum class TunnelState { - UP, - DOWN, - TOGGLE; + UP, + DOWN, + TOGGLE, + ; - fun toWgState(): Tunnel.State { - return when (this) { - UP -> Tunnel.State.UP - DOWN -> Tunnel.State.DOWN - TOGGLE -> Tunnel.State.TOGGLE - } - } + fun toWgState(): Tunnel.State { + return when (this) { + UP -> Tunnel.State.UP + DOWN -> Tunnel.State.DOWN + TOGGLE -> Tunnel.State.TOGGLE + } + } - fun toAmState(): org.amnezia.awg.backend.Tunnel.State { - return when (this) { - UP -> org.amnezia.awg.backend.Tunnel.State.UP - DOWN -> org.amnezia.awg.backend.Tunnel.State.DOWN - TOGGLE -> org.amnezia.awg.backend.Tunnel.State.TOGGLE - } - } + fun toAmState(): org.amnezia.awg.backend.Tunnel.State { + return when (this) { + UP -> org.amnezia.awg.backend.Tunnel.State.UP + DOWN -> org.amnezia.awg.backend.Tunnel.State.DOWN + TOGGLE -> org.amnezia.awg.backend.Tunnel.State.TOGGLE + } + } - companion object { - fun from(state: Tunnel.State): TunnelState { - return when (state) { - Tunnel.State.DOWN -> DOWN - Tunnel.State.TOGGLE -> TOGGLE - Tunnel.State.UP -> UP - } - } + companion object { + fun from(state: Tunnel.State): TunnelState { + return when (state) { + Tunnel.State.DOWN -> DOWN + Tunnel.State.TOGGLE -> TOGGLE + Tunnel.State.UP -> UP + } + } - fun from(state: org.amnezia.awg.backend.Tunnel.State): TunnelState { - return when (state) { - org.amnezia.awg.backend.Tunnel.State.DOWN -> DOWN - org.amnezia.awg.backend.Tunnel.State.TOGGLE -> TOGGLE - org.amnezia.awg.backend.Tunnel.State.UP -> UP - } - } - } + fun from(state: org.amnezia.awg.backend.Tunnel.State): TunnelState { + return when (state) { + org.amnezia.awg.backend.Tunnel.State.DOWN -> DOWN + org.amnezia.awg.backend.Tunnel.State.TOGGLE -> TOGGLE + org.amnezia.awg.backend.Tunnel.State.UP -> UP + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnService.kt index 67dfe5bf..311cbf64 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnService.kt @@ -5,11 +5,11 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import kotlinx.coroutines.flow.StateFlow interface VpnService : Tunnel, org.amnezia.awg.backend.Tunnel { - suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): TunnelState + suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): TunnelState - suspend fun stopTunnel() + suspend fun stopTunnel() - val vpnState: StateFlow + val vpnState: StateFlow - fun getState(): TunnelState + fun getState(): TunnelState } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnState.kt index 24b24a61..a28c46ca 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnState.kt @@ -4,7 +4,7 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics data class VpnState( - val status: TunnelState = TunnelState.DOWN, - val tunnelConfig: TunnelConfig? = null, - val statistics: TunnelStatistics? = null + val status: TunnelState = TunnelState.DOWN, + val tunnelConfig: TunnelConfig? = null, + val statistics: TunnelStatistics? = null, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt index 72309ff8..0046c22f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt @@ -32,191 +32,202 @@ import javax.inject.Provider class WireGuardTunnel @Inject constructor( - private val userspaceAmneziaBackend: Provider, - @Userspace private val userspaceBackend: Provider, - @Kernel private val kernelBackend: Provider, - private val appDataRepository: AppDataRepository, - @ApplicationScope private val applicationScope: CoroutineScope, - @IoDispatcher private val ioDispatcher: CoroutineDispatcher + private val userspaceAmneziaBackend: Provider, + @Userspace private val userspaceBackend: Provider, + @Kernel private val kernelBackend: Provider, + private val appDataRepository: AppDataRepository, + @ApplicationScope private val applicationScope: CoroutineScope, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, ) : VpnService { - private val _vpnState = MutableStateFlow(VpnState()) - override val vpnState: StateFlow = _vpnState.asStateFlow() + private val _vpnState = MutableStateFlow(VpnState()) + override val vpnState: StateFlow = _vpnState.asStateFlow() + private var statsJob: Job? = null - private var statsJob: Job? = null + private var backendIsWgUserspace = true - private var backendIsWgUserspace = true + private var backendIsAmneziaUserspace = false - private var backendIsAmneziaUserspace = false + init { + applicationScope.launch(ioDispatcher) { + appDataRepository.settings.getSettingsFlow().collect { + if (it.isKernelEnabled && (backendIsWgUserspace || backendIsAmneziaUserspace)) { + Timber.i("Setting kernel backend") + backendIsWgUserspace = false + backendIsAmneziaUserspace = false + } else if (!it.isKernelEnabled && !it.isAmneziaEnabled && !backendIsWgUserspace) { + Timber.i("Setting WireGuard userspace backend") + backendIsWgUserspace = true + backendIsAmneziaUserspace = false + } else if (it.isAmneziaEnabled && !backendIsAmneziaUserspace) { + Timber.i("Setting Amnezia userspace backend") + backendIsAmneziaUserspace = true + backendIsWgUserspace = false + } + } + } + } - init { - applicationScope.launch(ioDispatcher) { - appDataRepository.settings.getSettingsFlow().collect { - if (it.isKernelEnabled && (backendIsWgUserspace || backendIsAmneziaUserspace)) { - Timber.i("Setting kernel backend") - backendIsWgUserspace = false - backendIsAmneziaUserspace = false - } else if (!it.isKernelEnabled && !it.isAmneziaEnabled && !backendIsWgUserspace) { - Timber.i("Setting WireGuard userspace backend") - backendIsWgUserspace = true - backendIsAmneziaUserspace = false - } else if (it.isAmneziaEnabled && !backendIsAmneziaUserspace) { - Timber.i("Setting Amnezia userspace backend") - backendIsAmneziaUserspace = true - backendIsWgUserspace = false - } - } - } - } + private fun setState(tunnelConfig: TunnelConfig?, tunnelState: TunnelState): TunnelState { + return if (backendIsAmneziaUserspace) { + Timber.i("Using Amnezia backend") + val config = + tunnelConfig?.let { + if (it.amQuick != "") { + TunnelConfig.configFromAmQuick(it.amQuick) + } else { + Timber.w( + "Using backwards compatible wg config, amnezia specific config not found.", + ) + TunnelConfig.configFromAmQuick(it.wgQuick) + } + } + val state = + userspaceAmneziaBackend.get().setState(this, tunnelState.toAmState(), config) + TunnelState.from(state) + } else { + Timber.i("Using Wg backend") + val wgConfig = tunnelConfig?.let { TunnelConfig.configFromWgQuick(it.wgQuick) } + val state = + backend().setState( + this, + tunnelState.toWgState(), + wgConfig, + ) + TunnelState.from(state) + } + } - private fun setState(tunnelConfig: TunnelConfig?, tunnelState: TunnelState): TunnelState { - return if (backendIsAmneziaUserspace) { - Timber.i("Using Amnezia backend") - val config = tunnelConfig?.let { - if (it.amQuick != "") TunnelConfig.configFromAmQuick(it.amQuick) else { - Timber.w("Using backwards compatible wg config, amnezia specific config not found.") - TunnelConfig.configFromAmQuick(it.wgQuick) - } - } - val state = - userspaceAmneziaBackend.get().setState(this, tunnelState.toAmState(), config) - TunnelState.from(state) - } else { - Timber.i("Using Wg backend") - val wgConfig = tunnelConfig?.let { TunnelConfig.configFromWgQuick(it.wgQuick) } - val state = backend().setState( - this, - tunnelState.toWgState(), - wgConfig, - ) - TunnelState.from(state) - } - } + override suspend fun startTunnel(tunnelConfig: TunnelConfig?): TunnelState { + return withContext(ioDispatcher) { + try { + // TODO we need better error handling here + // need to bubble up these errors to the UI + val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel() + if (config != null) { + emitTunnelConfig(config) + setState(config, TunnelState.UP) + } else { + throw Exception("No tunnels") + } + } catch (e: BackendException) { + Timber.e("Failed to start tunnel with error: ${e.message}") + TunnelState.from(State.DOWN) + } + } + } - override suspend fun startTunnel(tunnelConfig: TunnelConfig?): TunnelState { - return withContext(ioDispatcher) { - try { - //TODO we need better error handling here - // need to bubble up these errors to the UI - val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel() - if (config != null) { - emitTunnelConfig(config) - setState(config, TunnelState.UP) - } else throw Exception("No tunnels") - } catch (e: BackendException) { - Timber.e("Failed to start tunnel with error: ${e.message}") - TunnelState.from(State.DOWN) - } - } - } + private fun backend(): Backend { + return when { + backendIsWgUserspace -> { + userspaceBackend.get() + } - private fun backend(): Backend { - return when { - backendIsWgUserspace -> { - userspaceBackend.get() - } + !backendIsWgUserspace && !backendIsAmneziaUserspace -> { + kernelBackend.get() + } - !backendIsWgUserspace && !backendIsAmneziaUserspace -> { - kernelBackend.get() - } + else -> { + userspaceBackend.get() + } + } + } - else -> { - userspaceBackend.get() - } - } - } + private fun emitTunnelState(state: TunnelState) { + _vpnState.tryEmit( + _vpnState.value.copy( + status = state, + ), + ) + } - private fun emitTunnelState(state: TunnelState) { - _vpnState.tryEmit( - _vpnState.value.copy( - status = state, - ), - ) - } + private fun emitBackendStatistics(statistics: TunnelStatistics) { + _vpnState.tryEmit( + _vpnState.value.copy( + statistics = statistics, + ), + ) + } - private fun emitBackendStatistics(statistics: TunnelStatistics) { - _vpnState.tryEmit( - _vpnState.value.copy( - statistics = statistics, - ), - ) - } + private suspend fun emitTunnelConfig(tunnelConfig: TunnelConfig?) { + _vpnState.emit( + _vpnState.value.copy( + tunnelConfig = tunnelConfig, + ), + ) + } - private suspend fun emitTunnelConfig(tunnelConfig: TunnelConfig?) { - _vpnState.emit( - _vpnState.value.copy( - tunnelConfig = tunnelConfig, - ), - ) - } + private fun resetVpnState() { + _vpnState.tryEmit(VpnState()) + } - private fun resetVpnState() { - _vpnState.tryEmit(VpnState()) - } + override suspend fun stopTunnel() { + withContext(ioDispatcher) { + try { + if (getState() == TunnelState.UP) { + val state = setState(null, TunnelState.DOWN) + resetVpnState() + emitTunnelState(state) + } + } catch (e: BackendException) { + Timber.e("Failed to stop wireguard tunnel with error: ${e.message}") + } catch (e: org.amnezia.awg.backend.BackendException) { + Timber.e("Failed to stop amnezia tunnel with error: ${e.message}") + } + } + } - override suspend fun stopTunnel() { - withContext(ioDispatcher) { - try { - if (getState() == TunnelState.UP) { - val state = setState(null, TunnelState.DOWN) - resetVpnState() - emitTunnelState(state) - } - } catch (e: BackendException) { - Timber.e("Failed to stop wireguard tunnel with error: ${e.message}") - } catch (e: org.amnezia.awg.backend.BackendException) { - Timber.e("Failed to stop amnezia tunnel with error: ${e.message}") - } - } - } + override fun getState(): TunnelState { + return if (backendIsAmneziaUserspace) { + TunnelState.from( + userspaceAmneziaBackend.get().getState(this), + ) + } else { + TunnelState.from(backend().getState(this)) + } + } - override fun getState(): TunnelState { - return if (backendIsAmneziaUserspace) TunnelState.from( - userspaceAmneziaBackend.get().getState(this), - ) - else TunnelState.from(backend().getState(this)) - } + override fun getName(): String { + return _vpnState.value.tunnelConfig?.name ?: "" + } - override fun getName(): String { - return _vpnState.value.tunnelConfig?.name ?: "" - } + override fun onStateChange(newState: Tunnel.State) { + handleStateChange(TunnelState.from(newState)) + } + private fun handleStateChange(state: TunnelState) { + emitTunnelState(state) + WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate() + if (state == TunnelState.UP) { + statsJob = startTunnelStatisticsJob() + } + if (state == TunnelState.DOWN) { + try { + statsJob?.cancel() + } catch (e: CancellationException) { + Timber.i("Stats job cancelled") + } + } + } - override fun onStateChange(newState: Tunnel.State) { - handleStateChange(TunnelState.from(newState)) - } + private fun startTunnelStatisticsJob() = applicationScope.launch(ioDispatcher) { + while (true) { + if (backendIsAmneziaUserspace) { + emitBackendStatistics( + AmneziaStatistics( + userspaceAmneziaBackend.get().getStatistics(this@WireGuardTunnel), + ), + ) + } else { + emitBackendStatistics( + WireGuardStatistics(backend().getStatistics(this@WireGuardTunnel)), + ) + } + delay(Constants.VPN_STATISTIC_CHECK_INTERVAL) + } + } - private fun handleStateChange(state: TunnelState) { - emitTunnelState(state) - WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate() - if (state == TunnelState.UP) { - statsJob = startTunnelStatisticsJob() - } - if (state == TunnelState.DOWN) { - try { - statsJob?.cancel() - } catch (e: CancellationException) { - Timber.i("Stats job cancelled") - } - } - } - - private fun startTunnelStatisticsJob() = applicationScope.launch(ioDispatcher) { - while (true) { - if (backendIsAmneziaUserspace) { - emitBackendStatistics( - AmneziaStatistics( - userspaceAmneziaBackend.get().getStatistics(this@WireGuardTunnel), - ), - ) - } else { - emitBackendStatistics(WireGuardStatistics(backend().getStatistics(this@WireGuardTunnel))) - } - delay(Constants.VPN_STATISTIC_CHECK_INTERVAL) - } - } - - override fun onStateChange(state: State) { - handleStateChange(TunnelState.from(state)) - } + override fun onStateChange(state: State) { + handleStateChange(TunnelState.from(state)) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/AmneziaStatistics.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/AmneziaStatistics.kt index a757322c..3b550a5b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/AmneziaStatistics.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/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/service/tunnel/statistics/TunnelStatistics.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/TunnelStatistics.kt index 4c70b343..5d108880 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/TunnelStatistics.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/TunnelStatistics.kt @@ -3,16 +3,16 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics 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/service/tunnel/statistics/WireGuardStatistics.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/WireGuardStatistics.kt index 266c6f8c..d77fe8ea 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/WireGuardStatistics.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/WireGuardStatistics.kt @@ -4,33 +4,33 @@ 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/AppUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppUiState.kt index f6cf152c..7da35d67 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppUiState.kt @@ -1,9 +1,9 @@ package com.zaneschepke.wireguardautotunnel.ui data class AppUiState( - val snackbarMessage: String = "", - val snackbarMessageConsumed: Boolean = true, - val vpnPermissionAccepted: Boolean = false, - val notificationPermissionAccepted: Boolean = false, - val requestPermissions: Boolean = false + val snackbarMessage: String = "", + val snackbarMessageConsumed: Boolean = true, + val vpnPermissionAccepted: Boolean = false, + val notificationPermissionAccepted: Boolean = false, + val requestPermissions: Boolean = false, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt index 440be752..d760d9e9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt @@ -20,104 +20,104 @@ import javax.inject.Inject class AppViewModel @Inject constructor() : ViewModel() { + val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance) - val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance) + private val _appUiState = + MutableStateFlow( + AppUiState( + vpnPermissionAccepted = vpnIntent == null, + ), + ) + val appUiState = _appUiState.asStateFlow() - private val _appUiState = MutableStateFlow( - AppUiState( - vpnPermissionAccepted = vpnIntent == null, - ), - ) - val appUiState = _appUiState.asStateFlow() + fun isRequiredPermissionGranted(): Boolean { + val allAccepted = + (_appUiState.value.vpnPermissionAccepted && _appUiState.value.vpnPermissionAccepted) + if (!allAccepted) requestPermissions() + return allAccepted + } + private fun requestPermissions() { + _appUiState.update { + it.copy( + requestPermissions = true, + ) + } + } - fun isRequiredPermissionGranted(): Boolean { - val allAccepted = - (_appUiState.value.vpnPermissionAccepted && _appUiState.value.vpnPermissionAccepted) - if (!allAccepted) requestPermissions() - return allAccepted - } + fun permissionsRequested() { + _appUiState.update { + it.copy( + requestPermissions = false, + ) + } + } - private fun requestPermissions() { - _appUiState.update { - it.copy( - requestPermissions = true, - ) - } - } + fun openWebPage(url: String, context: Context) { + try { + val webpage: Uri = Uri.parse(url) + val intent = + Intent(Intent.ACTION_VIEW, webpage).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(intent) + } catch (e: ActivityNotFoundException) { + Timber.e(e) + showSnackbarMessage(context.getString(R.string.no_browser_detected)) + } + } - fun permissionsRequested() { - _appUiState.update { - it.copy( - requestPermissions = false, - ) - } - } + fun onVpnPermissionAccepted() { + _appUiState.update { + it.copy( + vpnPermissionAccepted = true, + ) + } + } - fun openWebPage(url: String, context: Context) { - try { - val webpage: Uri = Uri.parse(url) - val intent = Intent(Intent.ACTION_VIEW, webpage).apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } - context.startActivity(intent) - } catch (e: ActivityNotFoundException) { - Timber.e(e) - showSnackbarMessage(context.getString(R.string.no_browser_detected)) - } - } + fun launchEmail(context: Context) { + try { + val intent = + Intent(Intent.ACTION_SENDTO).apply { + type = Constants.EMAIL_MIME_TYPE + putExtra(Intent.EXTRA_EMAIL, arrayOf(context.getString(R.string.my_email))) + putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject)) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity( + Intent.createChooser(intent, context.getString(R.string.email_chooser)).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + }, + ) + } catch (e: ActivityNotFoundException) { + Timber.e(e) + showSnackbarMessage(context.getString(R.string.no_email_detected)) + } + } - fun onVpnPermissionAccepted() { - _appUiState.update { - it.copy( - vpnPermissionAccepted = true, - ) - } - } + fun showSnackbarMessage(message: String) { + _appUiState.update { + it.copy( + snackbarMessage = message, + snackbarMessageConsumed = false, + ) + } + } - fun launchEmail(context: Context) { - try { - val intent = - Intent(Intent.ACTION_SENDTO).apply { - type = Constants.EMAIL_MIME_TYPE - putExtra(Intent.EXTRA_EMAIL, arrayOf(context.getString(R.string.my_email))) - putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject)) - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } - context.startActivity( - Intent.createChooser(intent, context.getString(R.string.email_chooser)).apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - }, - ) - } catch (e: ActivityNotFoundException) { - Timber.e(e) - showSnackbarMessage(context.getString(R.string.no_email_detected)) - } - } + fun snackbarMessageConsumed() { + _appUiState.update { + it.copy( + snackbarMessage = "", + snackbarMessageConsumed = true, + ) + } + } - fun showSnackbarMessage(message: String) { - _appUiState.update { - it.copy( - snackbarMessage = message, - snackbarMessageConsumed = false, - ) - } - } - - fun snackbarMessageConsumed() { - _appUiState.update { - it.copy( - snackbarMessage = "", - snackbarMessageConsumed = true, - ) - } - } - - fun setNotificationPermissionAccepted(accepted: Boolean) { - _appUiState.update { - it.copy( - notificationPermissionAccepted = accepted, - ) - } - } + fun setNotificationPermissionAccepted(accepted: Boolean) { + _appUiState.update { + it.copy( + notificationPermissionAccepted = accepted, + ) + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt index e8e3fdf3..c19a8824 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt @@ -65,225 +65,233 @@ import javax.inject.Inject @AndroidEntryPoint class MainActivity : AppCompatActivity() { + @Inject + lateinit var appStateRepository: AppStateRepository - @Inject - lateinit var appStateRepository: AppStateRepository + @Inject + lateinit var settingsRepository: SettingsRepository - @Inject - lateinit var settingsRepository: SettingsRepository + @Inject + lateinit var serviceManager: ServiceManager - @Inject - lateinit var serviceManager: ServiceManager + @OptIn( + ExperimentalPermissionsApi::class, + ) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) - @OptIn( - ExperimentalPermissionsApi::class, - ) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) + val isPinLockEnabled = intent.extras?.getBoolean(SplashActivity.IS_PIN_LOCK_ENABLED_KEY) - val isPinLockEnabled = intent.extras?.getBoolean(SplashActivity.IS_PIN_LOCK_ENABLED_KEY) + enableEdgeToEdge(navigationBarStyle = SystemBarStyle.dark(Color.Transparent.toArgb())) - enableEdgeToEdge(navigationBarStyle = SystemBarStyle.dark(Color.Transparent.toArgb())) + lifecycleScope.launch { + WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate() + val settings = settingsRepository.getSettings() + if (settings.isAutoTunnelEnabled) { + serviceManager.startWatcherService(application.applicationContext) + } + } - lifecycleScope.launch { - WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate() - val settings = settingsRepository.getSettings() - if (settings.isAutoTunnelEnabled) { - serviceManager.startWatcherService(application.applicationContext) - } - } + setContent { + val appViewModel = hiltViewModel() + val appUiState by appViewModel.appUiState.collectAsStateWithLifecycle() + val navController = rememberNavController() + val navBackStackEntry by navController.currentBackStackEntryAsState() - setContent { - val appViewModel = hiltViewModel() - val appUiState by appViewModel.appUiState.collectAsStateWithLifecycle() - val navController = rememberNavController() - val navBackStackEntry by navController.currentBackStackEntryAsState() + val notificationPermissionState = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) + } else { + null + } - val notificationPermissionState = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) - rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) else null + val snackbarHostState = remember { SnackbarHostState() } - val snackbarHostState = remember { SnackbarHostState() } + val vpnActivityResultState = + rememberLauncherForActivityResult( + ActivityResultContracts.StartActivityForResult(), + onResult = { + val accepted = (it.resultCode == RESULT_OK) + if (accepted) { + appViewModel.onVpnPermissionAccepted() + } + }, + ) - val vpnActivityResultState = - rememberLauncherForActivityResult( - ActivityResultContracts.StartActivityForResult(), - onResult = { - val accepted = (it.resultCode == RESULT_OK) - if (accepted) { - appViewModel.onVpnPermissionAccepted() - } - }, - ) + fun showSnackBarMessage(message: StringValue) { + lifecycleScope.launch(Dispatchers.Main) { + val result = + snackbarHostState.showSnackbar( + message = message.asString(this@MainActivity), + duration = SnackbarDuration.Short, + ) + when (result) { + SnackbarResult.ActionPerformed, + SnackbarResult.Dismissed, + -> { + snackbarHostState.currentSnackbarData?.dismiss() + } + } + } + } - fun showSnackBarMessage(message: StringValue) { - lifecycleScope.launch(Dispatchers.Main) { - val result = - snackbarHostState.showSnackbar( - message = message.asString(this@MainActivity), - duration = SnackbarDuration.Short, - ) - when (result) { - SnackbarResult.ActionPerformed, - SnackbarResult.Dismissed -> { - snackbarHostState.currentSnackbarData?.dismiss() - } - } - } - } + LaunchedEffect(appUiState.requestPermissions) { + if (appUiState.requestPermissions) { + appViewModel.permissionsRequested() + if (notificationPermissionState != null && !notificationPermissionState.status.isGranted + ) { + showSnackBarMessage( + StringValue.StringResource(R.string.notification_permission_required), + ) + return@LaunchedEffect notificationPermissionState.launchPermissionRequest() + } + if (!appUiState.vpnPermissionAccepted) { + return@LaunchedEffect appViewModel.vpnIntent?.let { + vpnActivityResultState.launch( + it, + ) + }!! + } + } + } - LaunchedEffect(appUiState.requestPermissions) { - if (appUiState.requestPermissions) { - appViewModel.permissionsRequested() - if (notificationPermissionState != null && !notificationPermissionState.status.isGranted - ) { - showSnackBarMessage(StringValue.StringResource(R.string.notification_permission_required)) - return@LaunchedEffect notificationPermissionState.launchPermissionRequest() - } - if (!appUiState.vpnPermissionAccepted) { - return@LaunchedEffect appViewModel.vpnIntent?.let { - vpnActivityResultState.launch( - it, - ) - }!! - } - } - } + WireguardAutoTunnelTheme { + LaunchedEffect(Unit) { + appViewModel.setNotificationPermissionAccepted( + notificationPermissionState?.status?.isGranted ?: true, + ) + } - WireguardAutoTunnelTheme { - LaunchedEffect(Unit) { - appViewModel.setNotificationPermissionAccepted( - notificationPermissionState?.status?.isGranted ?: true, - ) - } + LaunchedEffect(appUiState.snackbarMessageConsumed) { + if (!appUiState.snackbarMessageConsumed) { + showSnackBarMessage(StringValue.DynamicString(appUiState.snackbarMessage)) + appViewModel.snackbarMessageConsumed() + } + } - LaunchedEffect(appUiState.snackbarMessageConsumed) { - if (!appUiState.snackbarMessageConsumed) { - showSnackBarMessage(StringValue.DynamicString(appUiState.snackbarMessage)) - appViewModel.snackbarMessageConsumed() - } - } + val focusRequester = remember { FocusRequester() } - val focusRequester = remember { FocusRequester() } - - Scaffold( - snackbarHost = { - SnackbarHost(snackbarHostState) { snackbarData: SnackbarData -> - CustomSnackBar( - snackbarData.visuals.message, - isRtl = false, - containerColor = - MaterialTheme.colorScheme.surfaceColorAtElevation( - 2.dp, - ), - ) - } - }, - //TODO refactor - modifier = Modifier - .focusable() - .focusProperties { - when (navBackStackEntry?.destination?.route) { - Screen.Lock.route -> Unit - else -> up = focusRequester - } - }, - bottomBar = { - BottomNavBar( - navController, - listOf( - Screen.Main.navItem, - Screen.Settings.navItem, - Screen.Support.navItem, - ), - ) - }, - ) { padding -> - NavHost( - navController, - startDestination = (if (isPinLockEnabled == true) Screen.Lock.route else Screen.Main.route), - modifier = Modifier - .padding(padding) - .fillMaxSize(), - ) { - composable( - Screen.Main.route, - ) { - MainScreen( - focusRequester = focusRequester, - appViewModel = appViewModel, - navController = navController, - ) - } - composable( - Screen.Settings.route, - ) { - SettingsScreen( - appViewModel = appViewModel, - navController = navController, - focusRequester = focusRequester, - ) - } - composable( - Screen.Support.route, - ) { - SupportScreen( - focusRequester = focusRequester, - appViewModel = appViewModel, - navController = navController, - ) - } - composable(Screen.Support.Logs.route) { - LogsScreen() - } - composable( - "${Screen.Config.route}/{id}?configType={configType}", - arguments = - listOf( - navArgument("id") { - type = NavType.StringType - defaultValue = "0" - }, - navArgument("configType") { - type = NavType.StringType - defaultValue = ConfigType.WIREGUARD.name - }, - ), - ) { - val id = it.arguments?.getString("id") - val configType = ConfigType.valueOf( - it.arguments?.getString("configType") ?: ConfigType.WIREGUARD.name, - ) - if (!id.isNullOrBlank()) { - ConfigScreen( - navController = navController, - tunnelId = id, - appViewModel = appViewModel, - focusRequester = focusRequester, - configType = configType, - ) - } - } - composable("${Screen.Option.route}/{id}") { - val id = it.arguments?.getString("id") - if (!id.isNullOrBlank()) { - OptionsScreen( - navController = navController, - tunnelId = id, - appViewModel = appViewModel, - focusRequester = focusRequester, - ) - } - } - composable(Screen.Lock.route) { - PinLockScreen( - navController = navController, - appViewModel = appViewModel, - ) - } - } - } - } - } - } + Scaffold( + snackbarHost = { + SnackbarHost(snackbarHostState) { snackbarData: SnackbarData -> + CustomSnackBar( + snackbarData.visuals.message, + isRtl = false, + containerColor = + MaterialTheme.colorScheme.surfaceColorAtElevation( + 2.dp, + ), + ) + } + }, + // TODO refactor + modifier = + Modifier + .focusable() + .focusProperties { + when (navBackStackEntry?.destination?.route) { + Screen.Lock.route -> Unit + else -> up = focusRequester + } + }, + bottomBar = { + BottomNavBar( + navController, + listOf( + Screen.Main.navItem, + Screen.Settings.navItem, + Screen.Support.navItem, + ), + ) + }, + ) { padding -> + NavHost( + navController, + startDestination = (if (isPinLockEnabled == true) Screen.Lock.route else Screen.Main.route), + modifier = + Modifier + .padding(padding) + .fillMaxSize(), + ) { + composable( + Screen.Main.route, + ) { + MainScreen( + focusRequester = focusRequester, + appViewModel = appViewModel, + navController = navController, + ) + } + composable( + Screen.Settings.route, + ) { + SettingsScreen( + appViewModel = appViewModel, + navController = navController, + focusRequester = focusRequester, + ) + } + composable( + Screen.Support.route, + ) { + SupportScreen( + focusRequester = focusRequester, + appViewModel = appViewModel, + navController = navController, + ) + } + composable(Screen.Support.Logs.route) { + LogsScreen() + } + composable( + "${Screen.Config.route}/{id}?configType={configType}", + arguments = + listOf( + navArgument("id") { + type = NavType.StringType + defaultValue = "0" + }, + navArgument("configType") { + type = NavType.StringType + defaultValue = ConfigType.WIREGUARD.name + }, + ), + ) { + val id = it.arguments?.getString("id") + val configType = + ConfigType.valueOf( + it.arguments?.getString("configType") ?: ConfigType.WIREGUARD.name, + ) + if (!id.isNullOrBlank()) { + ConfigScreen( + navController = navController, + tunnelId = id, + appViewModel = appViewModel, + focusRequester = focusRequester, + configType = configType, + ) + } + } + composable("${Screen.Option.route}/{id}") { + val id = it.arguments?.getString("id") + if (!id.isNullOrBlank()) { + OptionsScreen( + navController = navController, + tunnelId = id, + appViewModel = appViewModel, + focusRequester = focusRequester, + ) + } + } + composable(Screen.Lock.route) { + PinLockScreen( + navController = navController, + appViewModel = appViewModel, + ) + } + } + } + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Screen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Screen.kt index 554a7594..465d179e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Screen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Screen.kt @@ -9,37 +9,38 @@ import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem sealed class Screen(val route: String) { - data object Main : Screen("main") { - val navItem = - BottomNavItem( - name = WireGuardAutoTunnel.instance.getString(R.string.tunnels), - route = route, - icon = Icons.Rounded.Home, - ) - } + data object Main : Screen("main") { + val navItem = + BottomNavItem( + name = WireGuardAutoTunnel.instance.getString(R.string.tunnels), + route = route, + icon = Icons.Rounded.Home, + ) + } - data object Settings : Screen("settings") { - val navItem = - BottomNavItem( - name = WireGuardAutoTunnel.instance.getString(R.string.settings), - route = route, - icon = Icons.Rounded.Settings, - ) - } + data object Settings : Screen("settings") { + val navItem = + BottomNavItem( + name = WireGuardAutoTunnel.instance.getString(R.string.settings), + route = route, + icon = Icons.Rounded.Settings, + ) + } - data object Support : Screen("support") { - val navItem = - BottomNavItem( - name = WireGuardAutoTunnel.instance.getString(R.string.support), - route = route, - icon = Icons.Rounded.QuestionMark, - ) + data object Support : Screen("support") { + val navItem = + BottomNavItem( + name = WireGuardAutoTunnel.instance.getString(R.string.support), + route = route, + icon = Icons.Rounded.QuestionMark, + ) - data object Logs : Screen("support/logs") - } + data object Logs : Screen("support/logs") + } - data object Config : Screen("config") - data object Lock : Screen("lock") + data object Config : Screen("config") - data object Option : Screen("option") + data object Lock : Screen("lock") + + data object Option : Screen("option") } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/SplashActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/SplashActivity.kt index 58ef902f..55b7ec1b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/SplashActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/SplashActivity.kt @@ -23,46 +23,45 @@ import javax.inject.Inject @SuppressLint("CustomSplashScreen") @AndroidEntryPoint class SplashActivity : ComponentActivity() { + @Inject + lateinit var appStateRepository: AppStateRepository - @Inject - lateinit var appStateRepository: AppStateRepository + @Inject + lateinit var localLogCollector: LocalLogCollector - @Inject - lateinit var localLogCollector: LocalLogCollector + @Inject + @ApplicationScope + lateinit var applicationScope: CoroutineScope - @Inject - @ApplicationScope - lateinit var applicationScope: CoroutineScope + override fun onCreate(savedInstanceState: Bundle?) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val splashScreen = installSplashScreen() + splashScreen.setKeepOnScreenCondition { true } + } + super.onCreate(savedInstanceState) - override fun onCreate(savedInstanceState: Bundle?) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - val splashScreen = installSplashScreen() - splashScreen.setKeepOnScreenCondition { true } - } - super.onCreate(savedInstanceState) + applicationScope.launch { + if (!isRunningOnAndroidTv()) localLogCollector.start() + } - applicationScope.launch { - if (!isRunningOnAndroidTv()) localLogCollector.start() - } + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + val pinLockEnabled = appStateRepository.isPinLockEnabled() + if (pinLockEnabled) { + PinManager.initialize(WireGuardAutoTunnel.instance) + } - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - val pinLockEnabled = appStateRepository.isPinLockEnabled() - if (pinLockEnabled) { - PinManager.initialize(WireGuardAutoTunnel.instance) - } + val intent = + Intent(this@SplashActivity, MainActivity::class.java).apply { + putExtra(IS_PIN_LOCK_ENABLED_KEY, pinLockEnabled) + } + startActivity(intent) + finish() + } + } + } - val intent = Intent(this@SplashActivity, MainActivity::class.java).apply { - putExtra(IS_PIN_LOCK_ENABLED_KEY, pinLockEnabled) - } - startActivity(intent) - finish() - } - } - } - - companion object { - const val IS_PIN_LOCK_ENABLED_KEY = "is_pin_lock_enabled" - } + companion object { + const val IS_PIN_LOCK_ENABLED_KEY = "is_pin_lock_enabled" + } } - diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt index 3e33f783..ae1f193b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt @@ -12,31 +12,25 @@ 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 -) { - TextButton( - onClick = onClick, - enabled = enabled, - ) { - Text(text, Modifier.weight(1f, false)) - 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) { + TextButton( + onClick = onClick, + enabled = enabled, + ) { + Text(text, Modifier.weight(1f, false)) + 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/RowListItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt index af570b4c..8acc2dd5 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt @@ -27,72 +27,73 @@ import com.zaneschepke.wireguardautotunnel.util.toThreeDecimalPlaceString @OptIn(ExperimentalFoundationApi::class) @Composable fun RowListItem( - icon: @Composable () -> Unit, - text: String, - onHold: () -> Unit, - onClick: () -> Unit, - rowButton: @Composable () -> Unit, - expanded: Boolean, - statistics: TunnelStatistics?, - focusRequester: FocusRequester, + icon: @Composable () -> Unit, + text: String, + onHold: () -> Unit, + onClick: () -> Unit, + rowButton: @Composable () -> Unit, + expanded: Boolean, + statistics: TunnelStatistics?, + focusRequester: FocusRequester, ) { - Box( - modifier = - Modifier - .focusRequester(focusRequester) - .animateContentSize() - .clip(RoundedCornerShape(30.dp)) - .combinedClickable( - onClick = { onClick() }, - onLongClick = { onHold() }, - ), - ) { - Column { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 15.dp, vertical = 5.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(13 / 20f), - ) { - icon() - Text(text, maxLines = 1, overflow = TextOverflow.Ellipsis) - } - rowButton() - } - if (expanded) { - statistics?.getPeers()?.forEach { - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(end = 10.dp, bottom = 10.dp, start = 10.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceEvenly, - ) { - //TODO change these to string resources - val handshakeEpoch = statistics.peerStats(it)!!.latestHandshakeEpochMillis - val peerTx = statistics.peerStats(it)!!.txBytes - val peerRx = statistics.peerStats(it)!!.rxBytes - val peerId = it.toBase64().subSequence(0, 3).toString() + "***" - val handshakeSec = - NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch) - val handshake = - if (handshakeSec == null) "never" else "$handshakeSec secs ago" - val peerTxMB = NumberUtils.bytesToMB(peerTx).toThreeDecimalPlaceString() - val peerRxMB = NumberUtils.bytesToMB(peerRx).toThreeDecimalPlaceString() - val fontSize = 9.sp - Text("peer: $peerId", fontSize = fontSize) - Text("handshake: $handshake", fontSize = fontSize) - Text("tx: $peerTxMB MB", fontSize = fontSize) - Text("rx: $peerRxMB MB", fontSize = fontSize) - } - } - } - } - } + Box( + modifier = + Modifier + .focusRequester(focusRequester) + .animateContentSize() + .clip(RoundedCornerShape(30.dp)) + .combinedClickable( + onClick = { onClick() }, + onLongClick = { onHold() }, + ), + ) { + Column { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 15.dp, vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(13 / 20f), + ) { + icon() + Text(text, maxLines = 1, overflow = TextOverflow.Ellipsis) + } + rowButton() + } + if (expanded) { + statistics?.getPeers()?.forEach { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(end = 10.dp, bottom = 10.dp, start = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + // TODO change these to string resources + val handshakeEpoch = statistics.peerStats(it)!!.latestHandshakeEpochMillis + val peerTx = statistics.peerStats(it)!!.txBytes + val peerRx = statistics.peerStats(it)!!.rxBytes + val peerId = it.toBase64().subSequence(0, 3).toString() + "***" + val handshakeSec = + NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch) + val handshake = + if (handshakeSec == null) "never" else "$handshakeSec secs ago" + val peerTxMB = NumberUtils.bytesToMB(peerTx).toThreeDecimalPlaceString() + val peerRxMB = NumberUtils.bytesToMB(peerRx).toThreeDecimalPlaceString() + val fontSize = 9.sp + Text("peer: $peerId", fontSize = fontSize) + Text("handshake: $handshake", fontSize = fontSize) + Text("tx: $peerTxMB MB", fontSize = fontSize) + Text("rx: $peerRxMB MB", fontSize = fontSize) + } + } + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/SearchBar.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/SearchBar.kt index fc774ea4..af2d7aef 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/SearchBar.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/SearchBar.kt @@ -26,57 +26,57 @@ import com.zaneschepke.wireguardautotunnel.R @Composable fun SearchBar(onQuery: (queryString: String) -> Unit) { - // Immediately update and keep track of query from text field changes. - var query: String by rememberSaveable { mutableStateOf("") } - var showClearIcon by rememberSaveable { mutableStateOf(false) } + // Immediately update and keep track of query from text field changes. + var query: String by rememberSaveable { mutableStateOf("") } + var showClearIcon by rememberSaveable { mutableStateOf(false) } - if (query.isEmpty()) { - showClearIcon = false - } else if (query.isNotEmpty()) { - showClearIcon = true - } + if (query.isEmpty()) { + showClearIcon = false + } else if (query.isNotEmpty()) { + showClearIcon = true + } - TextField( - value = query, - onValueChange = { onQueryChanged -> - // If user makes changes to text, immediately updated it. - query = onQueryChanged - onQuery(onQueryChanged) - }, - leadingIcon = { - val icon = Icons.Rounded.Search - Icon( - imageVector = icon, - tint = MaterialTheme.colorScheme.onBackground, - contentDescription = icon.name, - ) - }, - trailingIcon = { - if (showClearIcon) { - IconButton(onClick = { query = "" }) { - val icon = Icons.Rounded.Clear - Icon( - imageVector = icon, - tint = MaterialTheme.colorScheme.onBackground, - contentDescription = icon.name, - ) - } - } - }, - maxLines = 1, - colors = - TextFieldDefaults.colors( - focusedContainerColor = Color.Transparent, - unfocusedContainerColor = Color.Transparent, - disabledContainerColor = Color.Transparent, - ), - placeholder = { Text(text = stringResource(R.string.hint_search_packages)) }, - textStyle = MaterialTheme.typography.bodySmall, - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), - modifier = - Modifier - .fillMaxWidth() - .background(color = MaterialTheme.colorScheme.background, shape = RectangleShape), - ) + TextField( + value = query, + onValueChange = { onQueryChanged -> + // If user makes changes to text, immediately updated it. + query = onQueryChanged + onQuery(onQueryChanged) + }, + leadingIcon = { + val icon = Icons.Rounded.Search + Icon( + imageVector = icon, + tint = MaterialTheme.colorScheme.onBackground, + contentDescription = icon.name, + ) + }, + trailingIcon = { + if (showClearIcon) { + IconButton(onClick = { query = "" }) { + val icon = Icons.Rounded.Clear + Icon( + imageVector = icon, + tint = MaterialTheme.colorScheme.onBackground, + contentDescription = icon.name, + ) + } + } + }, + maxLines = 1, + colors = + TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + disabledContainerColor = Color.Transparent, + ), + placeholder = { Text(text = stringResource(R.string.hint_search_packages)) }, + textStyle = MaterialTheme.typography.bodySmall, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), + modifier = + Modifier + .fillMaxWidth() + .background(color = MaterialTheme.colorScheme.background, shape = RectangleShape), + ) } 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 616eabd3..24eae6c2 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 @@ -11,26 +11,26 @@ import androidx.compose.ui.text.input.KeyboardCapitalization @Composable fun ConfigurationTextBox( - value: String, - hint: String, - onValueChange: (String) -> Unit, - keyboardActions: KeyboardActions, - label: String, - modifier: Modifier + value: String, + hint: String, + onValueChange: (String) -> Unit, + keyboardActions: KeyboardActions, + label: String, + modifier: Modifier, ) { - OutlinedTextField( - modifier = modifier, - value = value, - singleLine = true, - onValueChange = { onValueChange(it) }, - label = { Text(label) }, - maxLines = 1, - placeholder = { Text(hint) }, - keyboardOptions = - KeyboardOptions( - capitalization = KeyboardCapitalization.None, - imeAction = ImeAction.Done, - ), - keyboardActions = keyboardActions, - ) + OutlinedTextField( + modifier = modifier, + value = value, + singleLine = true, + onValueChange = { onValueChange(it) }, + label = { Text(label) }, + maxLines = 1, + placeholder = { Text(hint) }, + keyboardOptions = + KeyboardOptions( + capitalization = KeyboardCapitalization.None, + imeAction = ImeAction.Done, + ), + keyboardActions = keyboardActions, + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationToggle.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationToggle.kt index b9bb4dcb..f9900302 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,35 +13,31 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp @Composable -fun ConfigurationToggle( - label: String, - enabled: Boolean, - checked: Boolean, - padding: Dp, - onCheckChanged: () -> Unit, - modifier: Modifier = Modifier -) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(padding), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - label, textAlign = TextAlign.Start, - modifier = Modifier - .weight( - weight = 1.0f, - fill = false, - ), - softWrap = true, - ) - Switch( - modifier = modifier, - enabled = enabled, - checked = checked, - onCheckedChange = { onCheckChanged() }, - ) - } +fun ConfigurationToggle(label: String, enabled: Boolean, checked: Boolean, padding: Dp, onCheckChanged: () -> Unit, modifier: Modifier = Modifier) { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(padding), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + label, + textAlign = TextAlign.Start, + modifier = + Modifier + .weight( + weight = 1.0f, + fill = false, + ), + softWrap = true, + ) + Switch( + modifier = modifier, + enabled = enabled, + checked = checked, + onCheckedChange = { onCheckChanged() }, + ) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt index 700d24de..3baf74f0 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt @@ -18,39 +18,42 @@ import com.zaneschepke.wireguardautotunnel.ui.Screen @Composable fun BottomNavBar(navController: NavController, bottomNavItems: List) { - val backStackEntry = navController.currentBackStackEntryAsState() + val backStackEntry = navController.currentBackStackEntryAsState() - var showBottomBar by rememberSaveable { mutableStateOf(true) } - val navBackStackEntry by navController.currentBackStackEntryAsState() + var showBottomBar by rememberSaveable { mutableStateOf(true) } + val navBackStackEntry by navController.currentBackStackEntryAsState() - //TODO find a better way to hide nav bar - showBottomBar = when (navBackStackEntry?.destination?.route) { - Screen.Lock.route -> false - else -> true - } + // TODO find a better way to hide nav bar + showBottomBar = + when (navBackStackEntry?.destination?.route) { + Screen.Lock.route -> false + else -> true + } - NavigationBar( - containerColor = if (!showBottomBar) Color.Transparent else MaterialTheme.colorScheme.background, - ) { - if (showBottomBar) bottomNavItems.forEach { item -> - val selected = item.route == backStackEntry.value?.destination?.route + NavigationBar( + containerColor = if (!showBottomBar) Color.Transparent else MaterialTheme.colorScheme.background, + ) { + if (showBottomBar) { + bottomNavItems.forEach { item -> + val selected = item.route == backStackEntry.value?.destination?.route - NavigationBarItem( - selected = selected, - onClick = { navController.navigate(item.route) }, - label = { - Text( - text = item.name, - fontWeight = FontWeight.SemiBold, - ) - }, - icon = { - Icon( - imageVector = item.icon, - contentDescription = "${item.name} Icon", - ) - }, - ) - } - } + NavigationBarItem( + selected = selected, + onClick = { navController.navigate(item.route) }, + label = { + Text( + text = item.name, + fontWeight = FontWeight.SemiBold, + ) + }, + icon = { + Icon( + imageVector = item.icon, + contentDescription = "${item.name} Icon", + ) + }, + ) + } + } + } } 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 b721c54e..125fb8cf 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 @@ -3,7 +3,7 @@ package com.zaneschepke.wireguardautotunnel.ui.common.navigation import androidx.compose.ui.graphics.vector.ImageVector data class BottomNavItem( - val name: String, - val route: String, - val icon: ImageVector, + val name: String, + val route: String, + val icon: ImageVector, ) 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 23540c55..2735a889 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 @@ -12,78 +12,77 @@ import androidx.fragment.app.FragmentActivity @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_HW_UNAVAILABLE -> { - onError("Biometrics not available") - 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_HW_UNAVAILABLE -> { + onError("Biometrics not available") + false + } - BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> { - onError("Biometrics not created") - false - } + BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> { + onError("Biometrics not created") + false + } - BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> { - onError("Biometric hardware not found") - false - } + BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> { + onError("Biometric hardware not found") + false + } - BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> { - onError("Biometric security update required") - false - } + BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> { + onError("Biometric security update required") + false + } - BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> { - onError("Biometrics not supported") - false - } + BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> { + onError("Biometrics not supported") + false + } - BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> { - onError("Biometrics status unknown") - false - } + BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> { + onError("Biometrics status unknown") + 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("Biometric Authentication") - .setSubtitle("Log in using your biometric credential") - .build() + val promptInfo = + BiometricPrompt.PromptInfo.Builder() + .setAllowedAuthenticators(BIOMETRIC_WEAK or DEVICE_CREDENTIAL) + .setTitle("Biometric Authentication") + .setSubtitle("Log in using your biometric credential") + .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/prompt/CustomSnackbar.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/CustomSnackbar.kt index d8811c58..1ad6dfbd 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/CustomSnackbar.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/CustomSnackbar.kt @@ -25,40 +25,37 @@ import androidx.compose.ui.unit.dp import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel @Composable -fun CustomSnackBar( - message: String, - isRtl: Boolean = true, - containerColor: Color = MaterialTheme.colorScheme.surface -) { - Snackbar( - containerColor = containerColor, - modifier = - Modifier - .fillMaxWidth( - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f, - ) - .padding(bottom = 100.dp), - shape = RoundedCornerShape(16.dp), - ) { - CompositionLocalProvider( - LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr, - ) { - 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 = Color.White, - modifier = Modifier.padding(end = 10.dp), - ) - Text(message, color = Color.White, modifier = Modifier.padding(end = 5.dp)) - } - } - } +fun CustomSnackBar(message: String, isRtl: Boolean = true, containerColor: Color = MaterialTheme.colorScheme.surface) { + Snackbar( + containerColor = containerColor, + modifier = + Modifier + .fillMaxWidth( + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f, + ) + .padding(bottom = 100.dp), + shape = RoundedCornerShape(16.dp), + ) { + CompositionLocalProvider( + LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr, + ) { + 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 = Color.White, + modifier = Modifier.padding(end = 10.dp), + ) + Text(message, color = Color.White, modifier = Modifier.padding(end = 5.dp)) + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/screen/LoadingScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/screen/LoadingScreen.kt index 97fb5de7..04ea15d8 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/screen/LoadingScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/screen/LoadingScreen.kt @@ -13,14 +13,15 @@ import androidx.compose.ui.unit.dp @Composable fun LoadingScreen() { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top, - modifier = Modifier - .fillMaxSize() - .focusable() - .padding(), - ) { - Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() } - } + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + modifier = + Modifier + .fillMaxSize() + .focusable() + .padding(), + ) { + Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/text/LogTypeLabel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/text/LogTypeLabel.kt index 334df0dd..1a55f7c8 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,13 +13,14 @@ 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/text/SectionTitle.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/text/SectionTitle.kt index 79caabb0..52b641cb 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/text/SectionTitle.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/text/SectionTitle.kt @@ -13,10 +13,10 @@ import androidx.compose.ui.unit.sp @Composable fun SectionTitle(title: String, padding: Dp) { - Text( - title, - textAlign = TextAlign.Start, - style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.ExtraBold), - modifier = Modifier.padding(padding, bottom = 5.dp, top = 5.dp), - ) + Text( + title, + textAlign = TextAlign.Start, + style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.ExtraBold), + modifier = Modifier.padding(padding, bottom = 5.dp, top = 5.dp), + ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt index a09833d0..e28b285d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt @@ -86,661 +86,733 @@ import kotlinx.coroutines.delay @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @OptIn( - ExperimentalMaterial3Api::class, + ExperimentalMaterial3Api::class, ) @Composable fun ConfigScreen( - viewModel: ConfigViewModel = hiltViewModel(), - focusRequester: FocusRequester, - navController: NavController, - appViewModel: AppViewModel, - tunnelId: String, - configType: ConfigType + viewModel: ConfigViewModel = hiltViewModel(), + focusRequester: FocusRequester, + navController: NavController, + appViewModel: AppViewModel, + tunnelId: String, + configType: ConfigType, ) { - val context = LocalContext.current - val clipboardManager: ClipboardManager = LocalClipboardManager.current - val keyboardController = LocalSoftwareKeyboardController.current - var showApplicationsDialog by remember { mutableStateOf(false) } - var showAuthPrompt by remember { mutableStateOf(false) } - var isAuthenticated by remember { mutableStateOf(false) } + val context = LocalContext.current + val clipboardManager: ClipboardManager = LocalClipboardManager.current + val keyboardController = LocalSoftwareKeyboardController.current + var showApplicationsDialog by remember { mutableStateOf(false) } + var showAuthPrompt by remember { mutableStateOf(false) } + var isAuthenticated by remember { mutableStateOf(false) } - val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() - LaunchedEffect(Unit) { viewModel.init(tunnelId) } + LaunchedEffect(Unit) { viewModel.init(tunnelId) } - LaunchedEffect(uiState.loading) { - if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) { - delay(Constants.FOCUS_REQUEST_DELAY) - focusRequester.requestFocus() - } - } + LaunchedEffect(uiState.loading) { + if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) { + delay(Constants.FOCUS_REQUEST_DELAY) + focusRequester.requestFocus() + } + } - if (uiState.loading) { - LoadingScreen() - return - } + if (uiState.loading) { + LoadingScreen() + return + } - val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }) + val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }) - val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done) + val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done) - val fillMaxHeight = .85f - val fillMaxWidth = .85f - val screenPadding = 5.dp + val fillMaxHeight = .85f + val fillMaxWidth = .85f + val screenPadding = 5.dp - val applicationButtonText = buildAnnotatedString { - append(stringResource(id = R.string.tunneling_apps)) - append(": ") - if (uiState.isAllApplicationsEnabled) { - append(stringResource(id = R.string.all)) - } else { - append("${uiState.checkedPackageNames.size} ") - (if (uiState.include) append(stringResource(id = R.string.included)) else append( - stringResource(id = R.string.excluded), - )) - } - } + val applicationButtonText = + buildAnnotatedString { + append(stringResource(id = R.string.tunneling_apps)) + append(": ") + if (uiState.isAllApplicationsEnabled) { + append(stringResource(id = R.string.all)) + } else { + append("${uiState.checkedPackageNames.size} ") + ( + if (uiState.include) { + append(stringResource(id = R.string.included)) + } else { + append( + stringResource(id = R.string.excluded), + ) + } + ) + } + } - if (showAuthPrompt) { - AuthorizationPrompt( - onSuccess = { - showAuthPrompt = false - isAuthenticated = true - }, - onError = { - showAuthPrompt = false - appViewModel.showSnackbarMessage(context.getString(R.string.error_authentication_failed)) - }, - onFailure = { - showAuthPrompt = false - appViewModel.showSnackbarMessage(context.getString(R.string.error_authorization_failed)) - }, - ) - } + if (showAuthPrompt) { + AuthorizationPrompt( + onSuccess = { + showAuthPrompt = false + isAuthenticated = true + }, + onError = { + showAuthPrompt = false + appViewModel.showSnackbarMessage( + context.getString(R.string.error_authentication_failed), + ) + }, + onFailure = { + showAuthPrompt = false + appViewModel.showSnackbarMessage( + context.getString(R.string.error_authorization_failed), + ) + }, + ) + } - if (showApplicationsDialog) { - val sortedPackages = - remember(uiState.packages) { - uiState.packages.sortedBy { viewModel.getPackageLabel(it) } - } - BasicAlertDialog(onDismissRequest = { showApplicationsDialog = false }) { - Surface( - tonalElevation = 2.dp, - shadowElevation = 2.dp, - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surface, - modifier = - Modifier - .fillMaxWidth() - .fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f), - ) { - Column( - modifier = Modifier - .fillMaxWidth(), - ) { - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp, vertical = 7.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text(stringResource(id = R.string.tunnel_all)) - Switch( - checked = uiState.isAllApplicationsEnabled, - onCheckedChange = { viewModel.onAllApplicationsChange(it) }, - ) - } - if (!uiState.isAllApplicationsEnabled) { - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp, vertical = 7.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text(stringResource(id = R.string.include)) - Checkbox( - checked = uiState.include, - onCheckedChange = { - viewModel.onIncludeChange(!uiState.include) - }, - ) - } - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text(stringResource(id = R.string.exclude)) - Checkbox( - checked = !uiState.include, - onCheckedChange = { - viewModel.onIncludeChange(!uiState.include) - }, - ) - } - } - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp, vertical = 7.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - SearchBar(viewModel::emitQueriedPackages) - } - Spacer(Modifier.padding(5.dp)) - LazyColumn( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Top, - modifier = Modifier.fillMaxHeight(4 / 5f), - ) { - items(sortedPackages, key = { it.packageName }) { pack -> - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier - .fillMaxSize() - .padding(5.dp), - ) { - Row(modifier = Modifier.fillMaxWidth(fillMaxWidth)) { - val drawable = - pack.applicationInfo?.loadIcon(context.packageManager) - if (drawable != null) { - Image( - painter = DrawablePainter(drawable), - stringResource(id = R.string.icon), - modifier = Modifier.size(50.dp, 50.dp), - ) - } else { - val icon = Icons.Rounded.Android - Icon( - icon, - icon.name, - modifier = Modifier.size(50.dp, 50.dp), - ) - } - Text( - viewModel.getPackageLabel(pack), - modifier = Modifier.padding(5.dp), - ) - } - Checkbox( - modifier = Modifier.fillMaxSize(), - checked = - (uiState.checkedPackageNames.contains( - pack.packageName, - )), - onCheckedChange = { - if (it) { - viewModel.onAddCheckedPackage(pack.packageName) - } else { - viewModel.onRemoveCheckedPackage(pack.packageName) - } - }, - ) - } - } - } - } - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxSize() - .padding(top = 5.dp), - horizontalArrangement = Arrangement.Center, - ) { - TextButton(onClick = { showApplicationsDialog = false }) { - Text(stringResource(R.string.done)) - } - } - } - } - } - } + if (showApplicationsDialog) { + val sortedPackages = + remember(uiState.packages) { + uiState.packages.sortedBy { viewModel.getPackageLabel(it) } + } + BasicAlertDialog(onDismissRequest = { showApplicationsDialog = false }) { + Surface( + tonalElevation = 2.dp, + shadowElevation = 2.dp, + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surface, + modifier = + Modifier + .fillMaxWidth() + .fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f), + ) { + Column( + modifier = + Modifier + .fillMaxWidth(), + ) { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(stringResource(id = R.string.tunnel_all)) + Switch( + checked = uiState.isAllApplicationsEnabled, + onCheckedChange = { viewModel.onAllApplicationsChange(it) }, + ) + } + if (!uiState.isAllApplicationsEnabled) { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(stringResource(id = R.string.include)) + Checkbox( + checked = uiState.include, + onCheckedChange = { + viewModel.onIncludeChange(!uiState.include) + }, + ) + } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(stringResource(id = R.string.exclude)) + Checkbox( + checked = !uiState.include, + onCheckedChange = { + viewModel.onIncludeChange(!uiState.include) + }, + ) + } + } + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + SearchBar(viewModel::emitQueriedPackages) + } + Spacer(Modifier.padding(5.dp)) + LazyColumn( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier.fillMaxHeight(4 / 5f), + ) { + items(sortedPackages, key = { it.packageName }) { pack -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = + Modifier + .fillMaxSize() + .padding(5.dp), + ) { + Row(modifier = Modifier.fillMaxWidth(fillMaxWidth)) { + val drawable = + pack.applicationInfo?.loadIcon(context.packageManager) + if (drawable != null) { + Image( + painter = DrawablePainter(drawable), + stringResource(id = R.string.icon), + modifier = Modifier.size(50.dp, 50.dp), + ) + } else { + val icon = Icons.Rounded.Android + Icon( + icon, + icon.name, + modifier = Modifier.size(50.dp, 50.dp), + ) + } + Text( + viewModel.getPackageLabel(pack), + modifier = Modifier.padding(5.dp), + ) + } + Checkbox( + modifier = Modifier.fillMaxSize(), + checked = + ( + uiState.checkedPackageNames.contains( + pack.packageName, + ) + ), + onCheckedChange = { + if (it) { + viewModel.onAddCheckedPackage(pack.packageName) + } else { + viewModel.onRemoveCheckedPackage(pack.packageName) + } + }, + ) + } + } + } + } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .fillMaxSize() + .padding(top = 5.dp), + horizontalArrangement = Arrangement.Center, + ) { + TextButton(onClick = { showApplicationsDialog = false }) { + Text(stringResource(R.string.done)) + } + } + } + } + } + } - Scaffold( - floatingActionButtonPosition = FabPosition.End, - floatingActionButton = { - val secondaryColor = MaterialTheme.colorScheme.secondary - val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) - var fobColor by remember { mutableStateOf(secondaryColor) } - FloatingActionButton( - modifier = - Modifier.onFocusChanged { - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - fobColor = if (it.isFocused) hoverColor else secondaryColor - } - }, - onClick = { - viewModel.onSaveAllChanges(configType).onSuccess { - appViewModel.showSnackbarMessage(context.getString(R.string.config_changes_saved)) - navController.navigate(Screen.Main.route) - }.onFailure { - appViewModel.showSnackbarMessage(it.getMessage(context)) - } - }, - containerColor = fobColor, - shape = RoundedCornerShape(16.dp), - ) { - Icon( - imageVector = Icons.Rounded.Save, - contentDescription = stringResource(id = R.string.save_changes), - tint = Color.DarkGray, - ) - } - }, - ) { - Column { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top, - modifier = - Modifier - .verticalScroll(rememberScrollState()) - .weight(1f, true) - .fillMaxSize(), - ) { - Surface( - tonalElevation = 2.dp, - shadowElevation = 2.dp, - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surface, - modifier = - (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - Modifier - .fillMaxHeight(fillMaxHeight) - .fillMaxWidth(fillMaxWidth) - } else { - Modifier.fillMaxWidth(fillMaxWidth) - }) - .padding(bottom = 10.dp), - ) { - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Top, - modifier = Modifier - .padding(15.dp) - .focusGroup(), - ) { - SectionTitle( - stringResource(R.string.interface_), - padding = screenPadding, - ) - ConfigurationTextBox( - value = uiState.tunnelName, - onValueChange = { value -> viewModel.onTunnelNameChange(value) }, - keyboardActions = keyboardActions, - label = stringResource(R.string.name), - hint = stringResource(R.string.tunnel_name).lowercase(), - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - ) - OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .clickable { showAuthPrompt = true }, - value = uiState.interfaceProxy.privateKey, - visualTransformation = - if ((tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated) - VisualTransformation.None - else PasswordVisualTransformation(), - enabled = (tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated, - onValueChange = { value -> viewModel.onPrivateKeyChange(value) }, - trailingIcon = { - IconButton( - modifier = Modifier.focusRequester(FocusRequester.Default), - onClick = { viewModel.generateKeyPair() }, - ) { - Icon( - Icons.Rounded.Refresh, - stringResource(R.string.rotate_keys), - tint = Color.White, - ) - } - }, - label = { Text(stringResource(R.string.private_key)) }, - singleLine = true, - placeholder = { Text(stringResource(R.string.base64_key)) }, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - ) - OutlinedTextField( - modifier = - Modifier - .fillMaxWidth() - .focusRequester(FocusRequester.Default), - value = uiState.interfaceProxy.publicKey, - enabled = false, - onValueChange = {}, - trailingIcon = { - IconButton( - modifier = Modifier.focusRequester(FocusRequester.Default), - onClick = { - clipboardManager.setText( - AnnotatedString(uiState.interfaceProxy.publicKey), - ) - }, - ) { - Icon( - Icons.Rounded.ContentCopy, - stringResource(R.string.copy_public_key), - tint = Color.White, - ) - } - }, - label = { Text(stringResource(R.string.public_key)) }, - singleLine = true, - placeholder = { Text(stringResource(R.string.base64_key)) }, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - ) - Row(modifier = Modifier.fillMaxWidth()) { - ConfigurationTextBox( - value = uiState.interfaceProxy.addresses, - onValueChange = { value -> viewModel.onAddressesChanged(value) }, - keyboardActions = keyboardActions, - label = stringResource(R.string.addresses), - hint = stringResource(R.string.comma_separated_list), - modifier = Modifier - .fillMaxWidth(3 / 5f) - .padding(end = 5.dp), - ) - ConfigurationTextBox( - value = uiState.interfaceProxy.listenPort, - onValueChange = { value -> viewModel.onListenPortChanged(value) }, - keyboardActions = keyboardActions, - label = stringResource(R.string.listen_port), - hint = stringResource(R.string.random), - modifier = Modifier.width(IntrinsicSize.Min), - ) - } - Row(modifier = Modifier.fillMaxWidth()) { - ConfigurationTextBox( - value = uiState.interfaceProxy.dnsServers, - onValueChange = { value -> viewModel.onDnsServersChanged(value) }, - keyboardActions = keyboardActions, - label = stringResource(R.string.dns_servers), - hint = stringResource(R.string.comma_separated_list), - modifier = Modifier - .fillMaxWidth(3 / 5f) - .padding(end = 5.dp), - ) - ConfigurationTextBox( - value = uiState.interfaceProxy.mtu, - onValueChange = { value -> viewModel.onMtuChanged(value) }, - keyboardActions = keyboardActions, - label = stringResource(R.string.mtu), - hint = stringResource(R.string.auto), - modifier = Modifier.width(IntrinsicSize.Min), - ) - } - if (configType == ConfigType.AMNEZIA) { - ConfigurationTextBox( - value = uiState.interfaceProxy.junkPacketCount, - onValueChange = { value -> viewModel.onJunkPacketCountChanged(value) }, - keyboardActions = keyboardActions, - label = stringResource(R.string.junk_packet_count), - hint = stringResource(R.string.junk_packet_count).lowercase(), - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - ) - ConfigurationTextBox( - value = uiState.interfaceProxy.junkPacketMinSize, - onValueChange = { value -> - viewModel.onJunkPacketMinSizeChanged( - value, - ) - }, - keyboardActions = keyboardActions, - label = stringResource(R.string.junk_packet_minimum_size), - hint = stringResource(R.string.junk_packet_minimum_size).lowercase(), - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - ) - ConfigurationTextBox( - value = uiState.interfaceProxy.junkPacketMaxSize, - onValueChange = { value -> - viewModel.onJunkPacketMaxSizeChanged( - value, - ) - }, - keyboardActions = keyboardActions, - label = stringResource(R.string.junk_packet_maximum_size), - hint = stringResource(R.string.junk_packet_maximum_size).lowercase(), - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - ) - ConfigurationTextBox( - value = uiState.interfaceProxy.initPacketJunkSize, - onValueChange = { value -> - viewModel.onInitPacketJunkSizeChanged( - value, - ) - }, - keyboardActions = keyboardActions, - label = stringResource(R.string.init_packet_junk_size), - hint = stringResource(R.string.init_packet_junk_size).lowercase(), - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - ) - ConfigurationTextBox( - value = uiState.interfaceProxy.responsePacketJunkSize, - onValueChange = { value -> viewModel.onResponsePacketJunkSize(value) }, - keyboardActions = keyboardActions, - label = stringResource(R.string.response_packet_junk_size), - hint = stringResource(R.string.response_packet_junk_size).lowercase(), - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - ) - ConfigurationTextBox( - value = uiState.interfaceProxy.initPacketMagicHeader, - onValueChange = { value -> viewModel.onInitPacketMagicHeader(value) }, - keyboardActions = keyboardActions, - label = stringResource(R.string.init_packet_magic_header), - hint = stringResource(R.string.init_packet_magic_header).lowercase(), - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - ) - ConfigurationTextBox( - value = uiState.interfaceProxy.responsePacketMagicHeader, - onValueChange = { value -> - viewModel.onResponsePacketMagicHeader( - value, - ) - }, - keyboardActions = keyboardActions, - label = stringResource(R.string.response_packet_magic_header), - hint = stringResource(R.string.response_packet_magic_header).lowercase(), - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - ) - ConfigurationTextBox( - value = uiState.interfaceProxy.underloadPacketMagicHeader, - onValueChange = { value -> - viewModel.onUnderloadPacketMagicHeader( - value, - ) - }, - keyboardActions = keyboardActions, - label = stringResource(R.string.underload_packet_magic_header), - hint = stringResource(R.string.underload_packet_magic_header).lowercase(), - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - ) - ConfigurationTextBox( - value = uiState.interfaceProxy.transportPacketMagicHeader, - onValueChange = { value -> - viewModel.onTransportPacketMagicHeader( - value, - ) - }, - keyboardActions = keyboardActions, - label = stringResource(R.string.transport_packet_magic_header), - hint = stringResource(R.string.transport_packet_magic_header).lowercase(), - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - ) - } - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxSize() - .padding(top = 5.dp), - horizontalArrangement = Arrangement.Center, - ) { - TextButton(onClick = { showApplicationsDialog = true }) { - Text(applicationButtonText.text) - } - } - } - } - uiState.proxyPeers.forEachIndexed { index, peer -> - Surface( - tonalElevation = 2.dp, - shadowElevation = 2.dp, - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surface, - modifier = - (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - Modifier - .fillMaxHeight(fillMaxHeight) - .fillMaxWidth(fillMaxWidth) - } else { - Modifier.fillMaxWidth(fillMaxWidth) - }) - .padding(top = 10.dp, bottom = 10.dp), - ) { - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Top, - modifier = Modifier - .padding(horizontal = 15.dp) - .padding(bottom = 10.dp), - ) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 5.dp), - ) { - SectionTitle( - stringResource(R.string.peer), - padding = screenPadding, - ) - IconButton(onClick = { viewModel.onDeletePeer(index) }) { - val icon = Icons.Rounded.Delete - Icon(icon, icon.name) - } - } + Scaffold( + floatingActionButtonPosition = FabPosition.End, + floatingActionButton = { + val secondaryColor = MaterialTheme.colorScheme.secondary + val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) + var fobColor by remember { mutableStateOf(secondaryColor) } + FloatingActionButton( + modifier = + Modifier.onFocusChanged { + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + fobColor = if (it.isFocused) hoverColor else secondaryColor + } + }, + onClick = { + viewModel.onSaveAllChanges(configType).onSuccess { + appViewModel.showSnackbarMessage( + context.getString(R.string.config_changes_saved), + ) + navController.navigate(Screen.Main.route) + }.onFailure { + appViewModel.showSnackbarMessage(it.getMessage(context)) + } + }, + containerColor = fobColor, + shape = RoundedCornerShape(16.dp), + ) { + Icon( + imageVector = Icons.Rounded.Save, + contentDescription = stringResource(id = R.string.save_changes), + tint = Color.DarkGray, + ) + } + }, + ) { + Column { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + modifier = + Modifier + .verticalScroll(rememberScrollState()) + .weight(1f, true) + .fillMaxSize(), + ) { + Surface( + tonalElevation = 2.dp, + shadowElevation = 2.dp, + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surface, + modifier = + ( + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + Modifier + .fillMaxHeight(fillMaxHeight) + .fillMaxWidth(fillMaxWidth) + } else { + Modifier.fillMaxWidth(fillMaxWidth) + } + ) + .padding(bottom = 10.dp), + ) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = + Modifier + .padding(15.dp) + .focusGroup(), + ) { + SectionTitle( + stringResource(R.string.interface_), + padding = screenPadding, + ) + ConfigurationTextBox( + value = uiState.tunnelName, + onValueChange = { value -> viewModel.onTunnelNameChange(value) }, + keyboardActions = keyboardActions, + label = stringResource(R.string.name), + hint = stringResource(R.string.tunnel_name).lowercase(), + modifier = + Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + ) + OutlinedTextField( + modifier = + Modifier + .fillMaxWidth() + .clickable { showAuthPrompt = true }, + value = uiState.interfaceProxy.privateKey, + visualTransformation = + if ((tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, + enabled = (tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated, + onValueChange = { value -> viewModel.onPrivateKeyChange(value) }, + trailingIcon = { + IconButton( + modifier = Modifier.focusRequester(FocusRequester.Default), + onClick = { viewModel.generateKeyPair() }, + ) { + Icon( + Icons.Rounded.Refresh, + stringResource(R.string.rotate_keys), + tint = Color.White, + ) + } + }, + label = { Text(stringResource(R.string.private_key)) }, + singleLine = true, + placeholder = { Text(stringResource(R.string.base64_key)) }, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + ) + OutlinedTextField( + modifier = + Modifier + .fillMaxWidth() + .focusRequester(FocusRequester.Default), + value = uiState.interfaceProxy.publicKey, + enabled = false, + onValueChange = {}, + trailingIcon = { + IconButton( + modifier = Modifier.focusRequester(FocusRequester.Default), + onClick = { + clipboardManager.setText( + AnnotatedString(uiState.interfaceProxy.publicKey), + ) + }, + ) { + Icon( + Icons.Rounded.ContentCopy, + stringResource(R.string.copy_public_key), + tint = Color.White, + ) + } + }, + label = { Text(stringResource(R.string.public_key)) }, + singleLine = true, + placeholder = { Text(stringResource(R.string.base64_key)) }, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + ) + Row(modifier = Modifier.fillMaxWidth()) { + ConfigurationTextBox( + value = uiState.interfaceProxy.addresses, + onValueChange = { value -> viewModel.onAddressesChanged(value) }, + keyboardActions = keyboardActions, + label = stringResource(R.string.addresses), + hint = stringResource(R.string.comma_separated_list), + modifier = + Modifier + .fillMaxWidth(3 / 5f) + .padding(end = 5.dp), + ) + ConfigurationTextBox( + value = uiState.interfaceProxy.listenPort, + onValueChange = { value -> viewModel.onListenPortChanged(value) }, + keyboardActions = keyboardActions, + label = stringResource(R.string.listen_port), + hint = stringResource(R.string.random), + modifier = Modifier.width(IntrinsicSize.Min), + ) + } + Row(modifier = Modifier.fillMaxWidth()) { + ConfigurationTextBox( + value = uiState.interfaceProxy.dnsServers, + onValueChange = { value -> viewModel.onDnsServersChanged(value) }, + keyboardActions = keyboardActions, + label = stringResource(R.string.dns_servers), + hint = stringResource(R.string.comma_separated_list), + modifier = + Modifier + .fillMaxWidth(3 / 5f) + .padding(end = 5.dp), + ) + ConfigurationTextBox( + value = uiState.interfaceProxy.mtu, + onValueChange = { value -> viewModel.onMtuChanged(value) }, + keyboardActions = keyboardActions, + label = stringResource(R.string.mtu), + hint = stringResource(R.string.auto), + modifier = Modifier.width(IntrinsicSize.Min), + ) + } + if (configType == ConfigType.AMNEZIA) { + ConfigurationTextBox( + value = uiState.interfaceProxy.junkPacketCount, + onValueChange = { + value -> + viewModel.onJunkPacketCountChanged(value) + }, + keyboardActions = keyboardActions, + label = stringResource(R.string.junk_packet_count), + hint = stringResource(R.string.junk_packet_count).lowercase(), + modifier = + Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + ) + ConfigurationTextBox( + value = uiState.interfaceProxy.junkPacketMinSize, + onValueChange = { value -> + viewModel.onJunkPacketMinSizeChanged( + value, + ) + }, + keyboardActions = keyboardActions, + label = stringResource(R.string.junk_packet_minimum_size), + hint = + stringResource( + R.string.junk_packet_minimum_size, + ).lowercase(), + modifier = + Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + ) + ConfigurationTextBox( + value = uiState.interfaceProxy.junkPacketMaxSize, + onValueChange = { value -> + viewModel.onJunkPacketMaxSizeChanged( + value, + ) + }, + keyboardActions = keyboardActions, + label = stringResource(R.string.junk_packet_maximum_size), + hint = + stringResource( + R.string.junk_packet_maximum_size, + ).lowercase(), + modifier = + Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + ) + ConfigurationTextBox( + value = uiState.interfaceProxy.initPacketJunkSize, + onValueChange = { value -> + viewModel.onInitPacketJunkSizeChanged( + value, + ) + }, + keyboardActions = keyboardActions, + label = stringResource(R.string.init_packet_junk_size), + hint = stringResource(R.string.init_packet_junk_size).lowercase(), + modifier = + Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + ) + ConfigurationTextBox( + value = uiState.interfaceProxy.responsePacketJunkSize, + onValueChange = { + value -> + viewModel.onResponsePacketJunkSize(value) + }, + keyboardActions = keyboardActions, + label = stringResource(R.string.response_packet_junk_size), + hint = + stringResource( + R.string.response_packet_junk_size, + ).lowercase(), + modifier = + Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + ) + ConfigurationTextBox( + value = uiState.interfaceProxy.initPacketMagicHeader, + onValueChange = { + value -> + viewModel.onInitPacketMagicHeader(value) + }, + keyboardActions = keyboardActions, + label = stringResource(R.string.init_packet_magic_header), + hint = + stringResource( + R.string.init_packet_magic_header, + ).lowercase(), + modifier = + Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + ) + ConfigurationTextBox( + value = uiState.interfaceProxy.responsePacketMagicHeader, + onValueChange = { value -> + viewModel.onResponsePacketMagicHeader( + value, + ) + }, + keyboardActions = keyboardActions, + label = stringResource(R.string.response_packet_magic_header), + hint = + stringResource( + R.string.response_packet_magic_header, + ).lowercase(), + modifier = + Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + ) + ConfigurationTextBox( + value = uiState.interfaceProxy.underloadPacketMagicHeader, + onValueChange = { value -> + viewModel.onUnderloadPacketMagicHeader( + value, + ) + }, + keyboardActions = keyboardActions, + label = stringResource(R.string.underload_packet_magic_header), + hint = + stringResource( + R.string.underload_packet_magic_header, + ).lowercase(), + modifier = + Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + ) + ConfigurationTextBox( + value = uiState.interfaceProxy.transportPacketMagicHeader, + onValueChange = { value -> + viewModel.onTransportPacketMagicHeader( + value, + ) + }, + keyboardActions = keyboardActions, + label = stringResource(R.string.transport_packet_magic_header), + hint = + stringResource( + R.string.transport_packet_magic_header, + ).lowercase(), + modifier = + Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + ) + } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .fillMaxSize() + .padding(top = 5.dp), + horizontalArrangement = Arrangement.Center, + ) { + TextButton(onClick = { showApplicationsDialog = true }) { + Text(applicationButtonText.text) + } + } + } + } + uiState.proxyPeers.forEachIndexed { index, peer -> + Surface( + tonalElevation = 2.dp, + shadowElevation = 2.dp, + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surface, + modifier = + ( + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + Modifier + .fillMaxHeight(fillMaxHeight) + .fillMaxWidth(fillMaxWidth) + } else { + Modifier.fillMaxWidth(fillMaxWidth) + } + ) + .padding(top = 10.dp, bottom = 10.dp), + ) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = + Modifier + .padding(horizontal = 15.dp) + .padding(bottom = 10.dp), + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 5.dp), + ) { + SectionTitle( + stringResource(R.string.peer), + padding = screenPadding, + ) + IconButton(onClick = { viewModel.onDeletePeer(index) }) { + val icon = Icons.Rounded.Delete + Icon(icon, icon.name) + } + } - ConfigurationTextBox( - value = peer.publicKey, - onValueChange = { value -> - viewModel.onPeerPublicKeyChange(index, value) - }, - keyboardActions = keyboardActions, - label = stringResource(R.string.public_key), - hint = stringResource(R.string.base64_key), - modifier = Modifier.fillMaxWidth(), - ) - ConfigurationTextBox( - value = peer.preSharedKey, - onValueChange = { value -> - viewModel.onPreSharedKeyChange(index, value) - }, - keyboardActions = keyboardActions, - label = stringResource(R.string.preshared_key), - hint = stringResource(R.string.optional), - modifier = Modifier.fillMaxWidth(), - ) - OutlinedTextField( - modifier = Modifier.fillMaxWidth(), - value = peer.persistentKeepalive, - enabled = true, - onValueChange = { value -> - viewModel.onPersistentKeepaliveChanged(index, value) - }, - trailingIcon = { - Text( - stringResource(R.string.seconds), - modifier = Modifier.padding(end = 10.dp), - ) - }, - label = { Text(stringResource(R.string.persistent_keepalive)) }, - singleLine = true, - placeholder = { - Text(stringResource(R.string.optional_no_recommend)) - }, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - ) - ConfigurationTextBox( - value = peer.endpoint, - onValueChange = { value -> - viewModel.onEndpointChange(index, value) - }, - keyboardActions = keyboardActions, - label = stringResource(R.string.endpoint), - hint = stringResource(R.string.endpoint).lowercase(), - modifier = Modifier.fillMaxWidth(), - ) - OutlinedTextField( - modifier = Modifier.fillMaxWidth(), - value = peer.allowedIps, - enabled = true, - onValueChange = { value -> - viewModel.onAllowedIpsChange(index, value) - }, - label = { Text(stringResource(R.string.allowed_ips)) }, - singleLine = true, - placeholder = { - Text(stringResource(R.string.comma_separated_list)) - }, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - ) - } - } - } - Row( - horizontalArrangement = Arrangement.SpaceEvenly, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxSize() - .padding(bottom = 140.dp), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, - ) { - TextButton(onClick = { viewModel.addEmptyPeer() }) { - Text(stringResource(R.string.add_peer)) - } - } - } - } - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - Spacer(modifier = Modifier.weight(.17f)) - } - } - } + ConfigurationTextBox( + value = peer.publicKey, + onValueChange = { value -> + viewModel.onPeerPublicKeyChange(index, value) + }, + keyboardActions = keyboardActions, + label = stringResource(R.string.public_key), + hint = stringResource(R.string.base64_key), + modifier = Modifier.fillMaxWidth(), + ) + ConfigurationTextBox( + value = peer.preSharedKey, + onValueChange = { value -> + viewModel.onPreSharedKeyChange(index, value) + }, + keyboardActions = keyboardActions, + label = stringResource(R.string.preshared_key), + hint = stringResource(R.string.optional), + modifier = Modifier.fillMaxWidth(), + ) + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = peer.persistentKeepalive, + enabled = true, + onValueChange = { value -> + viewModel.onPersistentKeepaliveChanged(index, value) + }, + trailingIcon = { + Text( + stringResource(R.string.seconds), + modifier = Modifier.padding(end = 10.dp), + ) + }, + label = { Text(stringResource(R.string.persistent_keepalive)) }, + singleLine = true, + placeholder = { + Text(stringResource(R.string.optional_no_recommend)) + }, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + ) + ConfigurationTextBox( + value = peer.endpoint, + onValueChange = { value -> + viewModel.onEndpointChange(index, value) + }, + keyboardActions = keyboardActions, + label = stringResource(R.string.endpoint), + hint = stringResource(R.string.endpoint).lowercase(), + modifier = Modifier.fillMaxWidth(), + ) + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = peer.allowedIps, + enabled = true, + onValueChange = { value -> + viewModel.onAllowedIpsChange(index, value) + }, + label = { Text(stringResource(R.string.allowed_ips)) }, + singleLine = true, + placeholder = { + Text(stringResource(R.string.comma_separated_list)) + }, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + ) + } + } + } + Row( + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .fillMaxSize() + .padding(bottom = 140.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + TextButton(onClick = { viewModel.addEmptyPeer() }) { + Text(stringResource(R.string.add_peer)) + } + } + } + } + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + Spacer(modifier = Modifier.weight(.17f)) + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigUiState.kt index aca6ec49..a9eb384e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigUiState.kt @@ -7,67 +7,67 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy import com.zaneschepke.wireguardautotunnel.util.Packages data class ConfigUiState( - val proxyPeers: List = arrayListOf(PeerProxy()), - val interfaceProxy: InterfaceProxy = InterfaceProxy(), - val packages: Packages = emptyList(), - val checkedPackageNames: List = emptyList(), - val include: Boolean = true, - val isAllApplicationsEnabled: Boolean = false, - val loading: Boolean = true, - val tunnel: TunnelConfig? = null, - val tunnelName: String = "", - val isAmneziaEnabled: Boolean = false + val proxyPeers: List = arrayListOf(PeerProxy()), + val interfaceProxy: InterfaceProxy = InterfaceProxy(), + val packages: Packages = emptyList(), + val checkedPackageNames: List = emptyList(), + val include: Boolean = true, + val isAllApplicationsEnabled: Boolean = false, + val loading: Boolean = true, + val tunnel: TunnelConfig? = null, + val tunnelName: String = "", + val isAmneziaEnabled: Boolean = false, ) { - companion object { - fun from(config: Config): ConfigUiState { - val proxyPeers = config.peers.map { PeerProxy.from(it) } - val proxyInterface = InterfaceProxy.from(config.`interface`) - var include = true - var isAllApplicationsEnabled = false - val checkedPackages = - if (config.`interface`.includedApplications.isNotEmpty()) { - config.`interface`.includedApplications - } else if (config.`interface`.excludedApplications.isNotEmpty()) { - include = false - config.`interface`.excludedApplications - } else { - isAllApplicationsEnabled = true - emptySet() - } - return ConfigUiState( - proxyPeers, - proxyInterface, - emptyList(), - checkedPackages.toList(), - include, - isAllApplicationsEnabled, - ) - } + companion object { + fun from(config: Config): ConfigUiState { + val proxyPeers = config.peers.map { PeerProxy.from(it) } + val proxyInterface = InterfaceProxy.from(config.`interface`) + var include = true + var isAllApplicationsEnabled = false + val checkedPackages = + if (config.`interface`.includedApplications.isNotEmpty()) { + config.`interface`.includedApplications + } else if (config.`interface`.excludedApplications.isNotEmpty()) { + include = false + config.`interface`.excludedApplications + } else { + isAllApplicationsEnabled = true + emptySet() + } + return ConfigUiState( + proxyPeers, + proxyInterface, + emptyList(), + checkedPackages.toList(), + include, + isAllApplicationsEnabled, + ) + } - fun from(config: org.amnezia.awg.config.Config): ConfigUiState { - //TODO update with new values - val proxyPeers = config.peers.map { PeerProxy.from(it) } - val proxyInterface = InterfaceProxy.from(config.`interface`) - var include = true - var isAllApplicationsEnabled = false - val checkedPackages = - if (config.`interface`.includedApplications.isNotEmpty()) { - config.`interface`.includedApplications - } else if (config.`interface`.excludedApplications.isNotEmpty()) { - include = false - config.`interface`.excludedApplications - } else { - isAllApplicationsEnabled = true - emptySet() - } - return ConfigUiState( - proxyPeers, - proxyInterface, - emptyList(), - checkedPackages.toList(), - include, - isAllApplicationsEnabled, - ) - } - } + fun from(config: org.amnezia.awg.config.Config): ConfigUiState { + // TODO update with new values + val proxyPeers = config.peers.map { PeerProxy.from(it) } + val proxyInterface = InterfaceProxy.from(config.`interface`) + var include = true + var isAllApplicationsEnabled = false + val checkedPackages = + if (config.`interface`.includedApplications.isNotEmpty()) { + config.`interface`.includedApplications + } else if (config.`interface`.excludedApplications.isNotEmpty()) { + include = false + config.`interface`.excludedApplications + } else { + isAllApplicationsEnabled = true + emptySet() + } + return ConfigUiState( + proxyPeers, + proxyInterface, + emptyList(), + checkedPackages.toList(), + include, + isAllApplicationsEnabled, + ) + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt index 22ba4949..1e29adf0 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt @@ -38,485 +38,529 @@ import javax.inject.Inject class ConfigViewModel @Inject constructor( - private val settingsRepository: SettingsRepository, - private val appDataRepository: AppDataRepository, - @IoDispatcher private val ioDispatcher: CoroutineDispatcher + private val settingsRepository: SettingsRepository, + private val appDataRepository: AppDataRepository, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, ) : ViewModel() { + private val packageManager = WireGuardAutoTunnel.instance.packageManager - private val packageManager = WireGuardAutoTunnel.instance.packageManager + private val _uiState = MutableStateFlow(ConfigUiState()) + val uiState = _uiState.asStateFlow() - private val _uiState = MutableStateFlow(ConfigUiState()) - val uiState = _uiState.asStateFlow() + fun init(tunnelId: String) = viewModelScope.launch(ioDispatcher) { + val packages = getQueriedPackages("") + val state = + if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) { + val tunnelConfig = + appDataRepository.tunnels.getAll() + .firstOrNull { it.id.toString() == tunnelId } + val isAmneziaEnabled = settingsRepository.getSettings().isAmneziaEnabled + if (tunnelConfig != null) { + ( + if (isAmneziaEnabled) { + val amConfig = + if (tunnelConfig.amQuick == "") tunnelConfig.wgQuick else tunnelConfig.amQuick + ConfigUiState.from(TunnelConfig.configFromAmQuick(amConfig)) + } else { + ConfigUiState.from( + TunnelConfig.configFromWgQuick(tunnelConfig.wgQuick), + ) + } + ).copy( + packages = packages, + loading = false, + tunnel = tunnelConfig, + tunnelName = tunnelConfig.name, + isAmneziaEnabled = isAmneziaEnabled, + ) + } else { + ConfigUiState(loading = false, packages = packages) + } + } else { + ConfigUiState(loading = false, packages = packages) + } + _uiState.value = state + } - fun init(tunnelId: String) = - viewModelScope.launch(ioDispatcher) { - val packages = getQueriedPackages("") - val state = - if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) { - val tunnelConfig = - appDataRepository.tunnels.getAll() - .firstOrNull { it.id.toString() == tunnelId } - val isAmneziaEnabled = settingsRepository.getSettings().isAmneziaEnabled - if (tunnelConfig != null) { - (if (isAmneziaEnabled) { - val amConfig = - if (tunnelConfig.amQuick == "") tunnelConfig.wgQuick else tunnelConfig.amQuick - ConfigUiState.from(TunnelConfig.configFromAmQuick(amConfig)) - } else ConfigUiState.from(TunnelConfig.configFromWgQuick(tunnelConfig.wgQuick))).copy( - packages = packages, - loading = false, - tunnel = tunnelConfig, - tunnelName = tunnelConfig.name, - isAmneziaEnabled = isAmneziaEnabled, - ) - } else { - ConfigUiState(loading = false, packages = packages) - } - } else { - ConfigUiState(loading = false, packages = packages) - } - _uiState.value = state - } + fun onTunnelNameChange(name: String) { + _uiState.value = _uiState.value.copy(tunnelName = name) + } - fun onTunnelNameChange(name: String) { - _uiState.value = _uiState.value.copy(tunnelName = name) - } + fun onIncludeChange(include: Boolean) { + _uiState.value = _uiState.value.copy(include = include) + } - fun onIncludeChange(include: Boolean) { - _uiState.value = _uiState.value.copy(include = include) - } + fun onAddCheckedPackage(packageName: String) { + _uiState.value = + _uiState.value.copy( + checkedPackageNames = _uiState.value.checkedPackageNames + packageName, + ) + } - fun onAddCheckedPackage(packageName: String) { - _uiState.value = - _uiState.value.copy( - checkedPackageNames = _uiState.value.checkedPackageNames + packageName, - ) - } + fun onAllApplicationsChange(isAllApplicationsEnabled: Boolean) { + _uiState.value = _uiState.value.copy(isAllApplicationsEnabled = isAllApplicationsEnabled) + } - fun onAllApplicationsChange(isAllApplicationsEnabled: Boolean) { - _uiState.value = _uiState.value.copy(isAllApplicationsEnabled = isAllApplicationsEnabled) - } + fun onRemoveCheckedPackage(packageName: String) { + _uiState.value = + _uiState.value.copy( + checkedPackageNames = _uiState.value.checkedPackageNames - packageName, + ) + } - fun onRemoveCheckedPackage(packageName: String) { - _uiState.value = - _uiState.value.copy( - checkedPackageNames = _uiState.value.checkedPackageNames - packageName, - ) - } + private fun getQueriedPackages(query: String): List { + return getAllInternetCapablePackages().filter { + getPackageLabel(it).lowercase().contains(query.lowercase()) + } + } - private fun getQueriedPackages(query: String): List { - return getAllInternetCapablePackages().filter { - getPackageLabel(it).lowercase().contains(query.lowercase()) - } - } + fun getPackageLabel(packageInfo: PackageInfo): String { + return packageInfo.applicationInfo?.loadLabel(packageManager).toString() + } - fun getPackageLabel(packageInfo: PackageInfo): String { - return packageInfo.applicationInfo?.loadLabel(packageManager).toString() - } + private fun getAllInternetCapablePackages(): List { + return getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET)) + } - private fun getAllInternetCapablePackages(): List { - return getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET)) - } + private fun getPackagesHoldingPermissions(permissions: Array): List { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + packageManager.getPackagesHoldingPermissions( + permissions, + PackageManager.PackageInfoFlags.of(0L), + ) + } else { + packageManager.getPackagesHoldingPermissions(permissions, 0) + } + } - private fun getPackagesHoldingPermissions(permissions: Array): List { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - packageManager.getPackagesHoldingPermissions( - permissions, - PackageManager.PackageInfoFlags.of(0L), - ) - } else { - packageManager.getPackagesHoldingPermissions(permissions, 0) - } - } + private fun isAllApplicationsEnabled(): Boolean { + return _uiState.value.isAllApplicationsEnabled + } - private fun isAllApplicationsEnabled(): Boolean { - return _uiState.value.isAllApplicationsEnabled - } + private fun saveConfig(tunnelConfig: TunnelConfig) = viewModelScope.launch { appDataRepository.tunnels.save(tunnelConfig) } - private fun saveConfig(tunnelConfig: TunnelConfig) = - viewModelScope.launch { appDataRepository.tunnels.save(tunnelConfig) } + private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) = viewModelScope.launch { + if (tunnelConfig != null) { + saveConfig(tunnelConfig).join() + WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate() + } + } - private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) = - viewModelScope.launch { - if (tunnelConfig != null) { - saveConfig(tunnelConfig).join() - WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate() - } - } + private fun buildPeerListFromProxyPeers(): List { + return _uiState.value.proxyPeers.map { + val builder = Peer.Builder() + if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim()) + if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim()) + if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.trim()) + if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.trim()) + if (it.persistentKeepalive.isNotEmpty()) { + builder.parsePersistentKeepalive(it.persistentKeepalive.trim()) + } + builder.build() + } + } - private fun buildPeerListFromProxyPeers(): List { - return _uiState.value.proxyPeers.map { - val builder = Peer.Builder() - if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim()) - if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim()) - if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.trim()) - if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.trim()) - if (it.persistentKeepalive.isNotEmpty()) { - builder.parsePersistentKeepalive(it.persistentKeepalive.trim()) - } - builder.build() - } - } + private fun buildAmPeerListFromProxyPeers(): List { + return _uiState.value.proxyPeers.map { + val builder = org.amnezia.awg.config.Peer.Builder() + if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim()) + if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim()) + if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.trim()) + if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.trim()) + if (it.persistentKeepalive.isNotEmpty()) { + builder.parsePersistentKeepalive(it.persistentKeepalive.trim()) + } + builder.build() + } + } - private fun buildAmPeerListFromProxyPeers(): List { - return _uiState.value.proxyPeers.map { - val builder = org.amnezia.awg.config.Peer.Builder() - if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim()) - if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim()) - if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.trim()) - if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.trim()) - if (it.persistentKeepalive.isNotEmpty()) { - builder.parsePersistentKeepalive(it.persistentKeepalive.trim()) - } - builder.build() - } - } + private fun emptyCheckedPackagesList() { + _uiState.value = _uiState.value.copy(checkedPackageNames = emptyList()) + } - private fun emptyCheckedPackagesList() { - _uiState.value = _uiState.value.copy(checkedPackageNames = emptyList()) - } + private fun buildInterfaceListFromProxyInterface(): Interface { + val builder = Interface.Builder() + builder.parsePrivateKey(_uiState.value.interfaceProxy.privateKey.trim()) + builder.parseAddresses(_uiState.value.interfaceProxy.addresses.trim()) + if (_uiState.value.interfaceProxy.dnsServers.isNotEmpty()) { + builder.parseDnsServers(_uiState.value.interfaceProxy.dnsServers.trim()) + } + if (_uiState.value.interfaceProxy.mtu.isNotEmpty()) { + builder.parseMtu(_uiState.value.interfaceProxy.mtu.trim()) + } + if (_uiState.value.interfaceProxy.listenPort.isNotEmpty()) { + builder.parseListenPort(_uiState.value.interfaceProxy.listenPort.trim()) + } + if (isAllApplicationsEnabled()) emptyCheckedPackagesList() + if (_uiState.value.include) { + builder.includeApplications( + _uiState.value.checkedPackageNames, + ) + } + if (!_uiState.value.include) { + builder.excludeApplications( + _uiState.value.checkedPackageNames, + ) + } + return builder.build() + } - private fun buildInterfaceListFromProxyInterface(): Interface { - val builder = Interface.Builder() - builder.parsePrivateKey(_uiState.value.interfaceProxy.privateKey.trim()) - builder.parseAddresses(_uiState.value.interfaceProxy.addresses.trim()) - if (_uiState.value.interfaceProxy.dnsServers.isNotEmpty()) { - builder.parseDnsServers(_uiState.value.interfaceProxy.dnsServers.trim()) - } - if (_uiState.value.interfaceProxy.mtu.isNotEmpty()) - builder.parseMtu(_uiState.value.interfaceProxy.mtu.trim()) - if (_uiState.value.interfaceProxy.listenPort.isNotEmpty()) { - builder.parseListenPort(_uiState.value.interfaceProxy.listenPort.trim()) - } - if (isAllApplicationsEnabled()) emptyCheckedPackagesList() - if (_uiState.value.include) builder.includeApplications(_uiState.value.checkedPackageNames) - if (!_uiState.value.include) builder.excludeApplications(_uiState.value.checkedPackageNames) - return builder.build() - } + private fun buildAmInterfaceListFromProxyInterface(): org.amnezia.awg.config.Interface { + val builder = org.amnezia.awg.config.Interface.Builder() + builder.parsePrivateKey(_uiState.value.interfaceProxy.privateKey.trim()) + builder.parseAddresses(_uiState.value.interfaceProxy.addresses.trim()) + if (_uiState.value.interfaceProxy.dnsServers.isNotEmpty()) { + builder.parseDnsServers(_uiState.value.interfaceProxy.dnsServers.trim()) + } + if (_uiState.value.interfaceProxy.mtu.isNotEmpty()) { + builder.parseMtu(_uiState.value.interfaceProxy.mtu.trim()) + } + if (_uiState.value.interfaceProxy.listenPort.isNotEmpty()) { + builder.parseListenPort(_uiState.value.interfaceProxy.listenPort.trim()) + } + if (isAllApplicationsEnabled()) emptyCheckedPackagesList() + if (_uiState.value.include) { + builder.includeApplications( + _uiState.value.checkedPackageNames, + ) + } + if (!_uiState.value.include) { + builder.excludeApplications( + _uiState.value.checkedPackageNames, + ) + } + if (_uiState.value.interfaceProxy.junkPacketCount.isNotEmpty()) { + builder.setJunkPacketCount( + _uiState.value.interfaceProxy.junkPacketCount.trim().toInt(), + ) + } + if (_uiState.value.interfaceProxy.junkPacketMinSize.isNotEmpty()) { + builder.setJunkPacketMinSize( + _uiState.value.interfaceProxy.junkPacketMinSize.trim().toInt(), + ) + } + if (_uiState.value.interfaceProxy.junkPacketMaxSize.isNotEmpty()) { + builder.setJunkPacketMaxSize( + _uiState.value.interfaceProxy.junkPacketMaxSize.trim().toInt(), + ) + } + if (_uiState.value.interfaceProxy.initPacketJunkSize.isNotEmpty()) { + builder.setInitPacketJunkSize( + _uiState.value.interfaceProxy.initPacketJunkSize.trim().toInt(), + ) + } + if (_uiState.value.interfaceProxy.responsePacketJunkSize.isNotEmpty()) { + builder.setResponsePacketJunkSize( + _uiState.value.interfaceProxy.responsePacketJunkSize.trim().toInt(), + ) + } + if (_uiState.value.interfaceProxy.initPacketMagicHeader.isNotEmpty()) { + builder.setInitPacketMagicHeader( + _uiState.value.interfaceProxy.initPacketMagicHeader.trim().toLong(), + ) + } + if (_uiState.value.interfaceProxy.responsePacketMagicHeader.isNotEmpty()) { + builder.setResponsePacketMagicHeader( + _uiState.value.interfaceProxy.responsePacketMagicHeader.trim().toLong(), + ) + } + if (_uiState.value.interfaceProxy.transportPacketMagicHeader.isNotEmpty()) { + builder.setTransportPacketMagicHeader( + _uiState.value.interfaceProxy.transportPacketMagicHeader.trim().toLong(), + ) + } + if (_uiState.value.interfaceProxy.underloadPacketMagicHeader.isNotEmpty()) { + builder.setUnderloadPacketMagicHeader( + _uiState.value.interfaceProxy.underloadPacketMagicHeader.trim().toLong(), + ) + } + return builder.build() + } - private fun buildAmInterfaceListFromProxyInterface(): org.amnezia.awg.config.Interface { - val builder = org.amnezia.awg.config.Interface.Builder() - builder.parsePrivateKey(_uiState.value.interfaceProxy.privateKey.trim()) - builder.parseAddresses(_uiState.value.interfaceProxy.addresses.trim()) - if (_uiState.value.interfaceProxy.dnsServers.isNotEmpty()) { - builder.parseDnsServers(_uiState.value.interfaceProxy.dnsServers.trim()) - } - if (_uiState.value.interfaceProxy.mtu.isNotEmpty()) - builder.parseMtu(_uiState.value.interfaceProxy.mtu.trim()) - if (_uiState.value.interfaceProxy.listenPort.isNotEmpty()) { - builder.parseListenPort(_uiState.value.interfaceProxy.listenPort.trim()) - } - if (isAllApplicationsEnabled()) emptyCheckedPackagesList() - if (_uiState.value.include) builder.includeApplications(_uiState.value.checkedPackageNames) - if (!_uiState.value.include) builder.excludeApplications(_uiState.value.checkedPackageNames) - if (_uiState.value.interfaceProxy.junkPacketCount.isNotEmpty()) { - builder.setJunkPacketCount(_uiState.value.interfaceProxy.junkPacketCount.trim().toInt()) - } - if (_uiState.value.interfaceProxy.junkPacketMinSize.isNotEmpty()) { - builder.setJunkPacketMinSize( - _uiState.value.interfaceProxy.junkPacketMinSize.trim().toInt(), - ) - } - if (_uiState.value.interfaceProxy.junkPacketMaxSize.isNotEmpty()) { - builder.setJunkPacketMaxSize( - _uiState.value.interfaceProxy.junkPacketMaxSize.trim().toInt(), - ) - } - if (_uiState.value.interfaceProxy.initPacketJunkSize.isNotEmpty()) { - builder.setInitPacketJunkSize( - _uiState.value.interfaceProxy.initPacketJunkSize.trim().toInt(), - ) - } - if (_uiState.value.interfaceProxy.responsePacketJunkSize.isNotEmpty()) { - builder.setResponsePacketJunkSize( - _uiState.value.interfaceProxy.responsePacketJunkSize.trim().toInt(), - ) - } - if (_uiState.value.interfaceProxy.initPacketMagicHeader.isNotEmpty()) { - builder.setInitPacketMagicHeader( - _uiState.value.interfaceProxy.initPacketMagicHeader.trim().toLong(), - ) - } - if (_uiState.value.interfaceProxy.responsePacketMagicHeader.isNotEmpty()) { - builder.setResponsePacketMagicHeader( - _uiState.value.interfaceProxy.responsePacketMagicHeader.trim().toLong(), - ) - } - if (_uiState.value.interfaceProxy.transportPacketMagicHeader.isNotEmpty()) { - builder.setTransportPacketMagicHeader( - _uiState.value.interfaceProxy.transportPacketMagicHeader.trim().toLong(), - ) - } - if (_uiState.value.interfaceProxy.underloadPacketMagicHeader.isNotEmpty()) { - builder.setUnderloadPacketMagicHeader( - _uiState.value.interfaceProxy.underloadPacketMagicHeader.trim().toLong(), - ) - } - return builder.build() - } + private fun buildConfig(): Config { + val peerList = buildPeerListFromProxyPeers() + val wgInterface = buildInterfaceListFromProxyInterface() + return Config.Builder().addPeers(peerList).setInterface(wgInterface).build() + } - private fun buildConfig(): Config { - val peerList = buildPeerListFromProxyPeers() - val wgInterface = buildInterfaceListFromProxyInterface() - return Config.Builder().addPeers(peerList).setInterface(wgInterface).build() - } + private fun buildAmConfig(): org.amnezia.awg.config.Config { + val peerList = buildAmPeerListFromProxyPeers() + val amInterface = buildAmInterfaceListFromProxyInterface() + return org.amnezia.awg.config.Config.Builder().addPeers( + peerList, + ).setInterface(amInterface) + .build() + } - private fun buildAmConfig(): org.amnezia.awg.config.Config { - val peerList = buildAmPeerListFromProxyPeers() - val amInterface = buildAmInterfaceListFromProxyInterface() - return org.amnezia.awg.config.Config.Builder().addPeers(peerList).setInterface(amInterface) - .build() - } + fun onSaveAllChanges(configType: ConfigType): Result { + return try { + val wgQuick = buildConfig().toWgQuickString() + val amQuick = + if (configType == ConfigType.AMNEZIA) { + buildAmConfig().toAwgQuickString() + } else { + TunnelConfig.AM_QUICK_DEFAULT + } + val tunnelConfig = + when (uiState.value.tunnel) { + null -> + TunnelConfig( + name = _uiState.value.tunnelName, + wgQuick = wgQuick, + amQuick = amQuick, + ) - fun onSaveAllChanges(configType: ConfigType): Result { - return try { - val wgQuick = buildConfig().toWgQuickString() - val amQuick = if (configType == ConfigType.AMNEZIA) { - buildAmConfig().toAwgQuickString() - } else TunnelConfig.AM_QUICK_DEFAULT - val tunnelConfig = when (uiState.value.tunnel) { - null -> TunnelConfig( - name = _uiState.value.tunnelName, - wgQuick = wgQuick, - amQuick = amQuick, - ) + else -> + uiState.value.tunnel!!.copy( + name = _uiState.value.tunnelName, + wgQuick = wgQuick, + amQuick = amQuick, + ) + } + updateTunnelConfig(tunnelConfig) + Result.success(Unit) + } catch (e: Exception) { + Timber.e(e) + val message = e.message?.substringAfter(":", missingDelimiterValue = "") + val stringValue = + message?.let { + StringValue.DynamicString(message) + } ?: StringValue.StringResource(R.string.unknown_error) + Result.failure(WgTunnelExceptions.ConfigParseError(stringValue)) + } + } - else -> uiState.value.tunnel!!.copy( - name = _uiState.value.tunnelName, - wgQuick = wgQuick, - amQuick = amQuick, - ) - } - updateTunnelConfig(tunnelConfig) - Result.success(Unit) - } catch (e: Exception) { - Timber.e(e) - val message = e.message?.substringAfter(":", missingDelimiterValue = "") - val stringValue = message?.let { - StringValue.DynamicString(message) - } ?: StringValue.StringResource(R.string.unknown_error) - Result.failure(WgTunnelExceptions.ConfigParseError(stringValue)) - } - } + fun onPeerPublicKeyChange(index: Int, value: String) { + _uiState.update { + it.copy( + proxyPeers = + _uiState.value.proxyPeers.update( + index, + _uiState.value.proxyPeers[index].copy(publicKey = value), + ), + ) + } + } - fun onPeerPublicKeyChange(index: Int, value: String) { - _uiState.update { - it.copy( - proxyPeers = - _uiState.value.proxyPeers.update( - index, - _uiState.value.proxyPeers[index].copy(publicKey = value), - ), - ) - } - } + fun onPreSharedKeyChange(index: Int, value: String) { + _uiState.update { + it.copy( + proxyPeers = + _uiState.value.proxyPeers.update( + index, + _uiState.value.proxyPeers[index].copy(preSharedKey = value), + ), + ) + } + } - fun onPreSharedKeyChange(index: Int, value: String) { - _uiState.update { - it.copy( - proxyPeers = - _uiState.value.proxyPeers.update( - index, - _uiState.value.proxyPeers[index].copy(preSharedKey = value), - ), - ) - } - } + fun onEndpointChange(index: Int, value: String) { + _uiState.update { + it.copy( + proxyPeers = + _uiState.value.proxyPeers.update( + index, + _uiState.value.proxyPeers[index].copy(endpoint = value), + ), + ) + } + } - fun onEndpointChange(index: Int, value: String) { - _uiState.update { - it.copy( - proxyPeers = - _uiState.value.proxyPeers.update( - index, - _uiState.value.proxyPeers[index].copy(endpoint = value), - ), - ) - } - } + fun onAllowedIpsChange(index: Int, value: String) { + _uiState.update { + it.copy( + proxyPeers = + _uiState.value.proxyPeers.update( + index, + _uiState.value.proxyPeers[index].copy(allowedIps = value), + ), + ) + } + } - fun onAllowedIpsChange(index: Int, value: String) { - _uiState.update { - it.copy( - proxyPeers = - _uiState.value.proxyPeers.update( - index, - _uiState.value.proxyPeers[index].copy(allowedIps = value), - ), - ) - } - } + fun onPersistentKeepaliveChanged(index: Int, value: String) { + _uiState.update { + it.copy( + proxyPeers = + _uiState.value.proxyPeers.update( + index, + _uiState.value.proxyPeers[index].copy(persistentKeepalive = value), + ), + ) + } + } - fun onPersistentKeepaliveChanged(index: Int, value: String) { - _uiState.update { - it.copy( - proxyPeers = - _uiState.value.proxyPeers.update( - index, - _uiState.value.proxyPeers[index].copy(persistentKeepalive = value), - ), - ) - } - } + fun onDeletePeer(index: Int) { + _uiState.update { + it.copy( + proxyPeers = _uiState.value.proxyPeers.removeAt(index), + ) + } + } - fun onDeletePeer(index: Int) { - _uiState.update { - it.copy( - proxyPeers = _uiState.value.proxyPeers.removeAt(index), - ) - } - } + fun addEmptyPeer() { + _uiState.update { + it.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy()) + } + } - fun addEmptyPeer() { - _uiState.update { - it.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy()) - } - } + fun generateKeyPair() { + val keyPair = KeyPair() + _uiState.update { + it.copy( + interfaceProxy = + _uiState.value.interfaceProxy.copy( + privateKey = keyPair.privateKey.toBase64(), + publicKey = keyPair.publicKey.toBase64(), + ), + ) + } + } - fun generateKeyPair() { - val keyPair = KeyPair() - _uiState.update { - it.copy( - interfaceProxy = - _uiState.value.interfaceProxy.copy( - privateKey = keyPair.privateKey.toBase64(), - publicKey = keyPair.publicKey.toBase64(), - ), - ) - } - } + fun onAddressesChanged(value: String) { + _uiState.update { + it.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value), + ) + } + } - fun onAddressesChanged(value: String) { - _uiState.update { - it.copy( - interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value), - ) - } + fun onListenPortChanged(value: String) { + _uiState.update { + it.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value), + ) + } + } - } + fun onDnsServersChanged(value: String) { + _uiState.update { + it.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value), + ) + } + } - fun onListenPortChanged(value: String) { - _uiState.update { - it.copy( - interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value), - ) - } - } + fun onMtuChanged(value: String) { + _uiState.update { + it.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value)) + } + } - fun onDnsServersChanged(value: String) { - _uiState.update { - it.copy( - interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value), - ) - } - } + private fun onInterfacePublicKeyChange(value: String) { + _uiState.update { + it.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value), + ) + } + } - fun onMtuChanged(value: String) { - _uiState.update { - it.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value)) - } - } + fun onPrivateKeyChange(value: String) { + _uiState.update { + it.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value), + ) + } + if (NumberUtils.isValidKey(value)) { + val pair = KeyPair(Key.fromBase64(value)) + onInterfacePublicKeyChange(pair.publicKey.toBase64()) + } else { + onInterfacePublicKeyChange("") + } + } - private fun onInterfacePublicKeyChange(value: String) { - _uiState.update { - it.copy( - interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value), - ) - } + fun emitQueriedPackages(query: String) { + val packages = + getAllInternetCapablePackages().filter { + getPackageLabel(it).lowercase().contains(query.lowercase()) + } + _uiState.update { it.copy(packages = packages) } + } - } + fun onJunkPacketCountChanged(value: String) { + _uiState.update { + it.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketCount = value), + ) + } + } - fun onPrivateKeyChange(value: String) { - _uiState.update { - it.copy( - interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value), - ) - } - if (NumberUtils.isValidKey(value)) { - val pair = KeyPair(Key.fromBase64(value)) - onInterfacePublicKeyChange(pair.publicKey.toBase64()) - } else { - onInterfacePublicKeyChange("") - } - } + fun onJunkPacketMinSizeChanged(value: String) { + _uiState.update { + it.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketMinSize = value), + ) + } + } - fun emitQueriedPackages(query: String) { - val packages = - getAllInternetCapablePackages().filter { - getPackageLabel(it).lowercase().contains(query.lowercase()) - } - _uiState.update { it.copy(packages = packages) } - } + fun onJunkPacketMaxSizeChanged(value: String) { + _uiState.update { + it.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketMaxSize = value), + ) + } + } - fun onJunkPacketCountChanged(value: String) { - _uiState.update { - it.copy( - interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketCount = value), - ) - } - } + fun onInitPacketJunkSizeChanged(value: String) { + _uiState.update { + it.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy(initPacketJunkSize = value), + ) + } + } - fun onJunkPacketMinSizeChanged(value: String) { - _uiState.update { - it.copy( - interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketMinSize = value), - ) - } - } + fun onResponsePacketJunkSize(value: String) { + _uiState.update { + it.copy( + interfaceProxy = + _uiState.value.interfaceProxy.copy( + responsePacketJunkSize = value, + ), + ) + } + } - fun onJunkPacketMaxSizeChanged(value: String) { - _uiState.update { - it.copy( - interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketMaxSize = value), - ) - } - } + fun onInitPacketMagicHeader(value: String) { + _uiState.update { + it.copy( + interfaceProxy = + _uiState.value.interfaceProxy.copy( + initPacketMagicHeader = value, + ), + ) + } + } - fun onInitPacketJunkSizeChanged(value: String) { - _uiState.update { - it.copy( - interfaceProxy = _uiState.value.interfaceProxy.copy(initPacketJunkSize = value), - ) - } - } + fun onResponsePacketMagicHeader(value: String) { + _uiState.update { + it.copy( + interfaceProxy = + _uiState.value.interfaceProxy.copy( + responsePacketMagicHeader = value, + ), + ) + } + } - fun onResponsePacketJunkSize(value: String) { - _uiState.update { - it.copy( - interfaceProxy = _uiState.value.interfaceProxy.copy(responsePacketJunkSize = value), - ) - } - } + fun onTransportPacketMagicHeader(value: String) { + _uiState.update { + it.copy( + interfaceProxy = + _uiState.value.interfaceProxy.copy( + transportPacketMagicHeader = value, + ), + ) + } + } - fun onInitPacketMagicHeader(value: String) { - _uiState.update { - it.copy( - interfaceProxy = _uiState.value.interfaceProxy.copy(initPacketMagicHeader = value), - ) - } - } - - fun onResponsePacketMagicHeader(value: String) { - _uiState.update { - it.copy( - interfaceProxy = _uiState.value.interfaceProxy.copy(responsePacketMagicHeader = value), - ) - } - } - - fun onTransportPacketMagicHeader(value: String) { - _uiState.update { - it.copy( - interfaceProxy = _uiState.value.interfaceProxy.copy(transportPacketMagicHeader = value), - ) - } - } - - fun onUnderloadPacketMagicHeader(value: String) { - _uiState.update { - it.copy( - interfaceProxy = _uiState.value.interfaceProxy.copy(underloadPacketMagicHeader = value), - ) - } - } + fun onUnderloadPacketMagicHeader(value: String) { + _uiState.update { + it.copy( + interfaceProxy = + _uiState.value.interfaceProxy.copy( + underloadPacketMagicHeader = value, + ), + ) + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/model/InterfaceProxy.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/model/InterfaceProxy.kt index 86dcbca0..88ec2dc8 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/model/InterfaceProxy.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/model/InterfaceProxy.kt @@ -3,71 +3,116 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.config.model import com.wireguard.config.Interface data class InterfaceProxy( - val privateKey: String = "", - val publicKey: String = "", - val addresses: String = "", - val dnsServers: String = "", - val listenPort: String = "", - val mtu: String = "", - 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 privateKey: String = "", + val publicKey: String = "", + val addresses: String = "", + val dnsServers: String = "", + val listenPort: String = "", + val mtu: String = "", + 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 = "", ) { - 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 = i.dnsServers.joinToString(", ").replace("/", "").trim(), - listenPort = - if (i.listenPort.isPresent) { - i.listenPort.get().toString().trim() - } else { - "" - }, - mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "", - ) - } + 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 = i.dnsServers.joinToString(", ").replace("/", "").trim(), + listenPort = + if (i.listenPort.isPresent) { + i.listenPort.get().toString().trim() + } else { + "" + }, + mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "", + ) + } - 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.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 "", - 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.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 "", + 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/screens/config/model/PeerProxy.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/model/PeerProxy.kt index 5223b54c..9e63266a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/model/PeerProxy.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/model/PeerProxy.kt @@ -3,96 +3,96 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.config.model import com.wireguard.config.Peer data class PeerProxy( - val publicKey: String = "", - val preSharedKey: String = "", - val persistentKeepalive: String = "", - val endpoint: String = "", - val allowedIps: String = IPV4_WILDCARD.joinToString(", ").trim() + val publicKey: String = "", + val preSharedKey: String = "", + val persistentKeepalive: String = "", + val endpoint: String = "", + val allowedIps: String = IPV4_WILDCARD.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(), - ) - } + 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(), - ) - } + 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(), + ) + } - val IPV4_PUBLIC_NETWORKS = - setOf( - "0.0.0.0/5", - "8.0.0.0/7", - "11.0.0.0/8", - "12.0.0.0/6", - "16.0.0.0/4", - "32.0.0.0/3", - "64.0.0.0/2", - "128.0.0.0/3", - "160.0.0.0/5", - "168.0.0.0/6", - "172.0.0.0/12", - "172.32.0.0/11", - "172.64.0.0/10", - "172.128.0.0/9", - "173.0.0.0/8", - "174.0.0.0/7", - "176.0.0.0/4", - "192.0.0.0/9", - "192.128.0.0/11", - "192.160.0.0/13", - "192.169.0.0/16", - "192.170.0.0/15", - "192.172.0.0/14", - "192.176.0.0/12", - "192.192.0.0/10", - "193.0.0.0/8", - "194.0.0.0/7", - "196.0.0.0/6", - "200.0.0.0/5", - "208.0.0.0/4", - ) - val IPV4_WILDCARD = setOf("0.0.0.0/0") - } + val IPV4_PUBLIC_NETWORKS = + setOf( + "0.0.0.0/5", + "8.0.0.0/7", + "11.0.0.0/8", + "12.0.0.0/6", + "16.0.0.0/4", + "32.0.0.0/3", + "64.0.0.0/2", + "128.0.0.0/3", + "160.0.0.0/5", + "168.0.0.0/6", + "172.0.0.0/12", + "172.32.0.0/11", + "172.64.0.0/10", + "172.128.0.0/9", + "173.0.0.0/8", + "174.0.0.0/7", + "176.0.0.0/4", + "192.0.0.0/9", + "192.128.0.0/11", + "192.160.0.0/13", + "192.169.0.0/16", + "192.170.0.0/15", + "192.172.0.0/14", + "192.176.0.0/12", + "192.192.0.0/10", + "193.0.0.0/8", + "194.0.0.0/7", + "196.0.0.0/6", + "200.0.0.0/5", + "208.0.0.0/4", + ) + val IPV4_WILDCARD = setOf("0.0.0.0/0") + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/ConfigType.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/ConfigType.kt index 67593b5d..863cfb6d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/ConfigType.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/ConfigType.kt @@ -1,6 +1,6 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main enum class ConfigType { - AMNEZIA, - WIREGUARD + AMNEZIA, + WIREGUARD, } 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 8156ad46..14eb1ec2 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 @@ -117,649 +117,684 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable fun MainScreen( - viewModel: MainViewModel = hiltViewModel(), - appViewModel: AppViewModel, - focusRequester: FocusRequester, - navController: NavController + viewModel: MainViewModel = hiltViewModel(), + appViewModel: AppViewModel, + focusRequester: FocusRequester, + navController: NavController, ) { - val haptic = LocalHapticFeedback.current - val context = LocalContext.current - val isVisible = rememberSaveable { mutableStateOf(true) } - val scope = rememberCoroutineScope() + val haptic = LocalHapticFeedback.current + val context = LocalContext.current + val isVisible = rememberSaveable { mutableStateOf(true) } + val scope = rememberCoroutineScope() - val sheetState = rememberModalBottomSheetState() - var showBottomSheet by remember { mutableStateOf(false) } - var configType by remember { mutableStateOf(ConfigType.WIREGUARD) } + val sheetState = rememberModalBottomSheetState() + var showBottomSheet by remember { mutableStateOf(false) } + var configType by remember { mutableStateOf(ConfigType.WIREGUARD) } - // Nested scroll for control FAB - val nestedScrollConnection = remember { - object : NestedScrollConnection { - override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { - // Hide FAB - if (available.y < -1) { - isVisible.value = false - } - // Show FAB - if (available.y > 1) { - isVisible.value = true - } + // Nested scroll for control FAB + val nestedScrollConnection = + remember { + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + // Hide FAB + if (available.y < -1) { + isVisible.value = false + } + // Show FAB + if (available.y > 1) { + isVisible.value = true + } - return Offset.Zero - } - } - } + return Offset.Zero + } + } + } + var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) } + var selectedTunnel by remember { mutableStateOf(null) } + val uiState by viewModel.uiState.collectAsStateWithLifecycle() - var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) } - var selectedTunnel by remember { mutableStateOf(null) } - val uiState by viewModel.uiState.collectAsStateWithLifecycle() + LaunchedEffect(Unit) { + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + delay(Constants.FOCUS_REQUEST_DELAY) + focusRequester.requestFocus() + } + } - LaunchedEffect(Unit) { - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - delay(Constants.FOCUS_REQUEST_DELAY) - focusRequester.requestFocus() - } - } - - val tunnelFileImportResultLauncher = - rememberLauncherForActivityResult( - object : ActivityResultContracts.GetContent() { - override fun createIntent(context: Context, input: String): Intent { - val intent = super.createIntent(context, input) + val tunnelFileImportResultLauncher = + rememberLauncherForActivityResult( + object : ActivityResultContracts.GetContent() { + override fun createIntent(context: Context, input: String): Intent { + val intent = super.createIntent(context, input) /* AndroidTV now comes with stubs that do nothing but display a Toast less helpful than * what we can do, so detect this and throw an exception that we can catch later. */ - val activitiesToResolveIntent = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - context.packageManager.queryIntentActivities( - intent, - PackageManager.ResolveInfoFlags.of( - PackageManager.MATCH_DEFAULT_ONLY.toLong(), - ), - ) - } else { - context.packageManager.queryIntentActivities( - intent, - PackageManager.MATCH_DEFAULT_ONLY, - ) - } - if ( - activitiesToResolveIntent.all { - val name = it.activityInfo.packageName - name.startsWith(Constants.GOOGLE_TV_EXPLORER_STUB) || - name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB) - } - ) { - appViewModel.showSnackbarMessage(context.getString(R.string.error_no_file_explorer)) - } - return intent - } - }, - ) { data -> - if (data == null) return@rememberLauncherForActivityResult - scope.launch { - viewModel.onTunnelFileSelected(data, configType, context).onFailure { - appViewModel.showSnackbarMessage(it.getMessage(context)) - } - } - } - val scanLauncher = - rememberLauncherForActivityResult( - contract = ScanContract(), - onResult = { - if (it.contents != null) { - scope.launch { - viewModel.onTunnelQrResult(it.contents, configType).onFailure { error -> - appViewModel.showSnackbarMessage(error.getMessage(context)) - } - } - } - }, - ) + 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) + } + ) { + appViewModel.showSnackbarMessage( + context.getString(R.string.error_no_file_explorer), + ) + } + return intent + } + }, + ) { data -> + if (data == null) return@rememberLauncherForActivityResult + scope.launch { + viewModel.onTunnelFileSelected(data, configType, context).onFailure { + appViewModel.showSnackbarMessage(it.getMessage(context)) + } + } + } + val scanLauncher = + rememberLauncherForActivityResult( + contract = ScanContract(), + onResult = { + if (it.contents != null) { + scope.launch { + viewModel.onTunnelQrResult(it.contents, configType).onFailure { error -> + appViewModel.showSnackbarMessage(error.getMessage(context)) + } + } + } + }, + ) - AnimatedVisibility(showDeleteTunnelAlertDialog) { - AlertDialog( - onDismissRequest = { showDeleteTunnelAlertDialog = false }, - confirmButton = { - TextButton( - onClick = { - selectedTunnel?.let { viewModel.onDelete(it, context) } - showDeleteTunnelAlertDialog = false - selectedTunnel = null - }, - ) { - Text(text = stringResource(R.string.yes)) - } - }, - dismissButton = { - TextButton(onClick = { showDeleteTunnelAlertDialog = false }) { - Text(text = stringResource(R.string.cancel)) - } - }, - title = { Text(text = stringResource(R.string.delete_tunnel)) }, - text = { Text(text = stringResource(R.string.delete_tunnel_message)) }, - ) - } + AnimatedVisibility(showDeleteTunnelAlertDialog) { + AlertDialog( + onDismissRequest = { showDeleteTunnelAlertDialog = false }, + confirmButton = { + TextButton( + onClick = { + selectedTunnel?.let { viewModel.onDelete(it, context) } + showDeleteTunnelAlertDialog = false + selectedTunnel = null + }, + ) { + Text(text = stringResource(R.string.yes)) + } + }, + dismissButton = { + TextButton(onClick = { showDeleteTunnelAlertDialog = false }) { + Text(text = stringResource(R.string.cancel)) + } + }, + title = { Text(text = stringResource(R.string.delete_tunnel)) }, + text = { Text(text = stringResource(R.string.delete_tunnel_message)) }, + ) + } - fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) { - if (appViewModel.isRequiredPermissionGranted()) { - if (checked) viewModel.onTunnelStart(tunnel, context) else viewModel.onTunnelStop( - context, - ) - } - } + fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) { + if (appViewModel.isRequiredPermissionGranted()) { + if (checked) { + viewModel.onTunnelStart(tunnel, context) + } else { + viewModel.onTunnelStop( + context, + ) + } + } + } - if (uiState.loading) { - return LoadingScreen() - } + if (uiState.loading) { + return LoadingScreen() + } - fun launchQrScanner() { - val scanOptions = ScanOptions() - scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE) - scanOptions.setOrientationLocked(true) - scanOptions.setPrompt( - context.getString(R.string.scanning_qr), - ) - scanOptions.setBeepEnabled(false) - scanLauncher.launch(scanOptions) - } + fun launchQrScanner() { + val scanOptions = ScanOptions() + scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE) + scanOptions.setOrientationLocked(true) + scanOptions.setPrompt( + context.getString(R.string.scanning_qr), + ) + scanOptions.setBeepEnabled(false) + scanLauncher.launch(scanOptions) + } - Scaffold( - modifier = - Modifier.pointerInput(Unit) { - if (uiState.tunnels.isNotEmpty()) { - detectTapGestures( - onTap = { - selectedTunnel = null - }, - ) - } + Scaffold( + modifier = + Modifier.pointerInput(Unit) { + if (uiState.tunnels.isNotEmpty()) { + detectTapGestures( + onTap = { + selectedTunnel = null + }, + ) + } + }, + floatingActionButtonPosition = FabPosition.End, + floatingActionButton = { + AnimatedVisibility( + visible = isVisible.value, + enter = slideInVertically(initialOffsetY = { it * 2 }), + exit = slideOutVertically(targetOffsetY = { it * 2 }), + modifier = + Modifier + .focusRequester(focusRequester) + .focusGroup(), + ) { + val secondaryColor = MaterialTheme.colorScheme.secondary + val tvFobColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) + val fobColor = + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) tvFobColor else secondaryColor + val fobIconColor = + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) Color.White else MaterialTheme.colorScheme.background + MultiFloatingActionButton( + fabIcon = + FabIcon( + iconRes = R.drawable.add, + iconResAfterRotate = R.drawable.close, + iconRotate = 180f, + ), + fabOption = + FabOption( + iconTint = fobIconColor, + backgroundTint = fobColor, + ), + itemsMultiFab = + listOf( + MultiFabItem( + label = { + Text( + stringResource(id = R.string.amnezia), + color = Color.White, + textAlign = TextAlign.Center, + modifier = Modifier.padding(end = 10.dp), + ) + }, + modifier = + Modifier + .size(40.dp), + icon = R.drawable.add, + value = ConfigType.AMNEZIA.name, + miniFabOption = + FabOption( + backgroundTint = fobColor, + fobIconColor, + ), + ), + MultiFabItem( + label = { + Text( + stringResource(id = R.string.wireguard), + color = Color.White, + textAlign = TextAlign.Center, + modifier = Modifier.padding(end = 10.dp), + ) + }, + icon = R.drawable.add, + value = ConfigType.WIREGUARD.name, + miniFabOption = + FabOption( + backgroundTint = fobColor, + fobIconColor, + ), + ), + ), + onFabItemClicked = { + showBottomSheet = true + configType = ConfigType.valueOf(it.value) + }, + shape = RoundedCornerShape(16.dp), + ) + } + }, + ) { + if (showBottomSheet) { + ModalBottomSheet( + onDismissRequest = { + showBottomSheet = false + }, + sheetState = sheetState, + ) { + // Sheet content + Row( + modifier = + Modifier + .fillMaxWidth() + .clickable { + showBottomSheet = false + tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES) + } + .padding(10.dp), + ) { + Icon( + Icons.Filled.FileOpen, + contentDescription = stringResource(id = R.string.open_file), + modifier = Modifier.padding(10.dp), + ) + Text( + stringResource(id = R.string.add_tunnels_text), + modifier = Modifier.padding(10.dp), + ) + } + if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) { + HorizontalDivider() + Row( + modifier = + Modifier + .fillMaxWidth() + .clickable { + scope.launch { + showBottomSheet = false + launchQrScanner() + } + } + .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 { + showBottomSheet = false + navController.navigate( + "${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}?configType=$configType", + ) + } + .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), + ) + } + } + } - }, - floatingActionButtonPosition = FabPosition.End, - floatingActionButton = { - AnimatedVisibility( - visible = isVisible.value, - enter = slideInVertically(initialOffsetY = { it * 2 }), - exit = slideOutVertically(targetOffsetY = { it * 2 }), - modifier = Modifier - .focusRequester(focusRequester) - .focusGroup(), - ) { - val secondaryColor = MaterialTheme.colorScheme.secondary - val tvFobColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) - val fobColor = - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) tvFobColor else secondaryColor - val fobIconColor = - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) Color.White else MaterialTheme.colorScheme.background - MultiFloatingActionButton( - fabIcon = FabIcon( - iconRes = R.drawable.add, - iconResAfterRotate = R.drawable.close, - iconRotate = 180f, - ), - fabOption = FabOption( - iconTint = fobIconColor, - backgroundTint = fobColor, - ), - itemsMultiFab = listOf( - MultiFabItem( - label = { - Text( - stringResource(id = R.string.amnezia), - color = Color.White, - textAlign = TextAlign.Center, - modifier = Modifier.padding(end = 10.dp), - ) - }, - modifier = Modifier - .size(40.dp), - icon = R.drawable.add, - value = ConfigType.AMNEZIA.name, - miniFabOption = FabOption( - backgroundTint = fobColor, - fobIconColor, - ), - ), - MultiFabItem( - label = { - Text( - stringResource(id = R.string.wireguard), - color = Color.White, - textAlign = TextAlign.Center, - modifier = Modifier.padding(end = 10.dp), - ) - }, - icon = R.drawable.add, - value = ConfigType.WIREGUARD.name, - miniFabOption = FabOption( - backgroundTint = fobColor, - fobIconColor, - ), - ), - ), - onFabItemClicked = { - showBottomSheet = true - configType = ConfigType.valueOf(it.value) - }, - shape = RoundedCornerShape(16.dp), - ) - } - }, - ) { - if (showBottomSheet) { - ModalBottomSheet( - onDismissRequest = { - showBottomSheet = false + LazyColumn( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = + Modifier + .fillMaxSize() + .overscroll(ScrollableDefaults.overscrollEffect()) + .nestedScroll(nestedScrollConnection), + state = rememberLazyListState(0, uiState.tunnels.count()), + userScrollEnabled = true, + reverseLayout = false, + flingBehavior = ScrollableDefaults.flingBehavior(), + ) { + item { + AnimatedVisibility( + uiState.tunnels.isEmpty(), + exit = fadeOut(), + enter = fadeIn(), + ) { + 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 -> + appViewModel.openWebPage(annotation.item, context) + } + } + } + } + } + item { + if (uiState.settings.isAutoTunnelEnabled) { + val itemFocusRequester = remember { FocusRequester() } + val autoTunnelingLabel = + buildAnnotatedString { + append(stringResource(id = R.string.auto_tunneling)) + append(": ") + if (uiState.settings.isAutoTunnelPaused) { + append( + stringResource(id = R.string.paused), + ) + } else { + append( + stringResource(id = R.string.active), + ) + } + } + RowListItem( + icon = { + val icon = Icons.Rounded.Bolt + Icon( + icon, + icon.name, + modifier = + Modifier + .padding(end = 8.5.dp) + .size(25.dp), + tint = + if (uiState.settings.isAutoTunnelPaused) { + Color.Gray + } else { + mint + }, + ) + }, + text = autoTunnelingLabel.text, + rowButton = { + if (uiState.settings.isAutoTunnelPaused) { + TextButton( + modifier = Modifier.focusRequester(itemFocusRequester), + onClick = { viewModel.resumeAutoTunneling() }, + ) { + Text(stringResource(id = R.string.resume)) + } + } else { + TextButton( + modifier = Modifier.focusRequester(itemFocusRequester), + onClick = { viewModel.pauseAutoTunneling() }, + ) { + Text(stringResource(id = R.string.pause)) + } + } + }, + onClick = { + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + itemFocusRequester.requestFocus() + } + }, + onHold = {}, + expanded = false, + statistics = null, + focusRequester = focusRequester, + ) + } + } + items( + uiState.tunnels, + key = { tunnel -> tunnel.id }, + ) { tunnel -> + val leadingIconColor = + ( + if ( + uiState.vpnState.tunnelConfig?.name == tunnel.name && + uiState.vpnState.status == TunnelState.UP + ) { + uiState.vpnState.statistics + ?.mapPeerStats() + ?.map { it.value?.handshakeStatus() } + .let { statuses -> + when { + statuses?.all { it == HandshakeStatus.HEALTHY } == true -> mint + statuses?.any { it == HandshakeStatus.STALE } == true -> corn + statuses?.all { it == HandshakeStatus.NOT_STARTED } == true -> + Color.Gray - }, - sheetState = sheetState, - ) { - // Sheet content - Row( - modifier = - Modifier - .fillMaxWidth() - .clickable { - showBottomSheet = false - tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES) - } - .padding(10.dp), - ) { - Icon( - Icons.Filled.FileOpen, - contentDescription = stringResource(id = R.string.open_file), - modifier = Modifier.padding(10.dp), - ) - Text( - stringResource(id = R.string.add_tunnels_text), - modifier = Modifier.padding(10.dp), - ) - } - if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) { - HorizontalDivider() - Row( - modifier = - Modifier - .fillMaxWidth() - .clickable { - scope.launch { - showBottomSheet = false - launchQrScanner() - } - } - .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 { - showBottomSheet = false - navController.navigate( - "${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}?configType=${configType}", - ) - } - .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), - ) - } - } - } + else -> { + Color.Gray + } + } + } + } else { + Color.Gray + } + ) + val itemFocusRequester = remember { FocusRequester() } + val expanded = remember { mutableStateOf(false) } + RowListItem( + icon = { + val circleIcon = Icons.Rounded.Circle + val icon = + if (tunnel.isPrimaryTunnel) { + Icons.Rounded.Star + } else if (tunnel.isMobileDataTunnel) { + Icons.Rounded.Smartphone + } else { + circleIcon + } + Icon( + icon, + icon.name, + tint = leadingIconColor, + modifier = + Modifier + .padding( + end = if (icon == circleIcon) 12.5.dp else 10.dp, + start = if (icon == circleIcon) 2.5.dp else 0.dp, + ) + .size(if (icon == circleIcon) 15.dp else 20.dp), + ) + }, + text = tunnel.name, + onHold = { + if ( + (uiState.vpnState.status == TunnelState.UP) && + (tunnel.name == uiState.vpnState.tunnelConfig?.name) + ) { + appViewModel.showSnackbarMessage( + context.getString(R.string.turn_off_tunnel), + ) + return@RowListItem + } + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + selectedTunnel = tunnel + }, + onClick = { + if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) { + if ( + uiState.vpnState.status == TunnelState.UP && + (uiState.vpnState.tunnelConfig?.name == tunnel.name) + ) { + expanded.value = !expanded.value + } + } else { + selectedTunnel = tunnel + itemFocusRequester.requestFocus() + } + }, + statistics = uiState.vpnState.statistics, + expanded = expanded.value, + focusRequester = focusRequester, + rowButton = { + if ( + tunnel.id == selectedTunnel?.id && + !WireGuardAutoTunnel.isRunningOnAndroidTv() + ) { + Row { + IconButton( + onClick = { + if ( + uiState.settings.isAutoTunnelEnabled && + !uiState.settings.isAutoTunnelPaused + ) { + appViewModel.showSnackbarMessage( + context.getString(R.string.turn_off_tunnel), + ) + } else { + navController.navigate( + "${Screen.Option.route}/${selectedTunnel?.id}", + ) + } + }, + ) { + val icon = Icons.Rounded.Settings + Icon( + icon, + icon.name, + ) + } + IconButton( + modifier = Modifier.focusable(), + onClick = { viewModel.onCopyTunnel(selectedTunnel) }, + ) { + val icon = Icons.Rounded.CopyAll + Icon(icon, icon.name) + } + IconButton( + modifier = Modifier.focusable(), + onClick = { showDeleteTunnelAlertDialog = true }, + ) { + val icon = Icons.Rounded.Delete + Icon(icon, icon.name) + } + } + } else { + val checked by remember { + derivedStateOf { + ( + uiState.vpnState.status == TunnelState.UP && + tunnel.name == uiState.vpnState.tunnelConfig?.name + ) + } + } + if (!checked) expanded.value = false - LazyColumn( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Top, - modifier = - Modifier - .fillMaxSize() - .overscroll(ScrollableDefaults.overscrollEffect()) - .nestedScroll(nestedScrollConnection), - state = rememberLazyListState(0, uiState.tunnels.count()), - userScrollEnabled = true, - reverseLayout = false, - flingBehavior = ScrollableDefaults.flingBehavior(), - ) { - item { - AnimatedVisibility( - uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn(), - ) { - 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 -> - appViewModel.openWebPage(annotation.item, context) - } - } - } - } - } - item { - if (uiState.settings.isAutoTunnelEnabled) { - val itemFocusRequester = remember { FocusRequester() } - val autoTunnelingLabel = buildAnnotatedString { - append(stringResource(id = R.string.auto_tunneling)) - append(": ") - if (uiState.settings.isAutoTunnelPaused) append( - stringResource(id = R.string.paused), - ) else append( - stringResource(id = R.string.active), - ) - } - RowListItem( - icon = { - val icon = Icons.Rounded.Bolt - Icon( - icon, - icon.name, - modifier = Modifier - .padding(end = 8.5.dp) - .size(25.dp), - tint = - if (uiState.settings.isAutoTunnelPaused) Color.Gray - else mint, - ) - }, - text = autoTunnelingLabel.text, - rowButton = { - if (uiState.settings.isAutoTunnelPaused) { - TextButton( - modifier = Modifier.focusRequester(itemFocusRequester), - onClick = { viewModel.resumeAutoTunneling() }, - ) { - Text(stringResource(id = R.string.resume)) - } - } else { - TextButton( - modifier = Modifier.focusRequester(itemFocusRequester), - onClick = { viewModel.pauseAutoTunneling() }, - ) { - Text(stringResource(id = R.string.pause)) - } - } - }, - onClick = { - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - itemFocusRequester.requestFocus() - } - }, - onHold = {}, - expanded = false, - statistics = null, - focusRequester = focusRequester, - ) - } - } - items( - uiState.tunnels, - key = { tunnel -> tunnel.id }, - ) { tunnel -> - val leadingIconColor = - (if ( - uiState.vpnState.tunnelConfig?.name == tunnel.name && - uiState.vpnState.status == TunnelState.UP - ) { - uiState.vpnState.statistics - ?.mapPeerStats() - ?.map { it.value?.handshakeStatus() } - .let { statuses -> - when { - statuses?.all { it == HandshakeStatus.HEALTHY } == true -> mint - statuses?.any { it == HandshakeStatus.STALE } == true -> corn - statuses?.all { it == HandshakeStatus.NOT_STARTED } == true -> - Color.Gray - - else -> { - Color.Gray - } - } - } - } else { - Color.Gray - }) - val itemFocusRequester = remember { FocusRequester() } - val expanded = remember { mutableStateOf(false) } - RowListItem( - icon = { - val circleIcon = Icons.Rounded.Circle - val icon = if (tunnel.isPrimaryTunnel) { - Icons.Rounded.Star - } else if (tunnel.isMobileDataTunnel) { - Icons.Rounded.Smartphone - } else { - circleIcon - } - Icon( - icon, - icon.name, - tint = leadingIconColor, - modifier = Modifier - .padding( - end = if (icon == circleIcon) 12.5.dp else 10.dp, - start = if (icon == circleIcon) 2.5.dp else 0.dp, - ) - .size(if (icon == circleIcon) 15.dp else 20.dp), - ) - }, - text = tunnel.name, - onHold = { - if ( - (uiState.vpnState.status == TunnelState.UP) && - (tunnel.name == uiState.vpnState.tunnelConfig?.name) - ) { - appViewModel.showSnackbarMessage(context.getString(R.string.turn_off_tunnel)) - return@RowListItem - } - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - selectedTunnel = tunnel - }, - onClick = { - if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) { - if ( - uiState.vpnState.status == TunnelState.UP && - (uiState.vpnState.tunnelConfig?.name == tunnel.name) - ) { - expanded.value = !expanded.value - } - } else { - selectedTunnel = tunnel - itemFocusRequester.requestFocus() - } - }, - statistics = uiState.vpnState.statistics, - expanded = expanded.value, - focusRequester = focusRequester, - rowButton = { - if ( - tunnel.id == selectedTunnel?.id && - !WireGuardAutoTunnel.isRunningOnAndroidTv() - ) { - Row { - IconButton( - onClick = { - if ( - uiState.settings.isAutoTunnelEnabled && - !uiState.settings.isAutoTunnelPaused - ) { - appViewModel.showSnackbarMessage( - context.getString(R.string.turn_off_tunnel), - ) - } else { - navController.navigate( - "${Screen.Option.route}/${selectedTunnel?.id}", - ) - } - }, - ) { - val icon = Icons.Rounded.Settings - Icon( - icon, - icon.name, - ) - } - IconButton( - modifier = Modifier.focusable(), - onClick = { viewModel.onCopyTunnel(selectedTunnel) }, - ) { - val icon = Icons.Rounded.CopyAll - Icon(icon, icon.name) - } - IconButton( - modifier = Modifier.focusable(), - onClick = { showDeleteTunnelAlertDialog = true }, - ) { - val icon = Icons.Rounded.Delete - Icon(icon, icon.name) - } - } - } else { - val checked by remember { - derivedStateOf { - (uiState.vpnState.status == TunnelState.UP && - tunnel.name == uiState.vpnState.tunnelConfig?.name) - } - } - if (!checked) expanded.value = false - - @Composable - fun TunnelSwitch() = - Switch( - modifier = Modifier.focusRequester(itemFocusRequester), - checked = checked, - onCheckedChange = { checked -> - if (!checked) expanded.value = false - onTunnelToggle(checked, tunnel) - }, - ) - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - Row { - IconButton( - onClick = { - if (uiState.settings.isAutoTunnelEnabled && !uiState.settings.isAutoTunnelPaused) { - appViewModel.showSnackbarMessage( - context.getString(R.string.turn_off_auto), - ) - } else { - selectedTunnel = tunnel - navController.navigate( - "${Screen.Option.route}/${selectedTunnel?.id}", - ) - } - }, - ) { - val icon = Icons.Rounded.Settings - Icon( - icon, - icon.name, - ) - } - IconButton( - modifier = Modifier.focusRequester(focusRequester), - onClick = { - if ( - uiState.vpnState.status == TunnelState.UP && - (uiState.vpnState.tunnelConfig?.name == tunnel.name) - ) { - expanded.value = !expanded.value - } else { - appViewModel.showSnackbarMessage( - context.getString(R.string.turn_on_tunnel), - ) - } - }, - ) { - val icon = Icons.Rounded.Info - Icon(icon, icon.name) - } - IconButton( - onClick = { viewModel.onCopyTunnel(tunnel) }, - ) { - val icon = Icons.Rounded.CopyAll - Icon(icon, icon.name) - } - IconButton( - onClick = { - if ( - uiState.vpnState.status == TunnelState.UP && - tunnel.name == uiState.vpnState.tunnelConfig?.name - ) { - appViewModel.showSnackbarMessage( - context.getString(R.string.turn_off_tunnel), - ) - } else { - selectedTunnel = tunnel - showDeleteTunnelAlertDialog = true - } - }, - ) { - val icon = Icons.Rounded.Delete - Icon( - icon, - icon.name, - ) - } - TunnelSwitch() - } - } else { - TunnelSwitch() - } - } - }, - ) - } - } - } + @Composable + fun TunnelSwitch() = Switch( + modifier = Modifier.focusRequester(itemFocusRequester), + checked = checked, + onCheckedChange = { checked -> + if (!checked) expanded.value = false + onTunnelToggle(checked, tunnel) + }, + ) + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + Row { + IconButton( + onClick = { + if (uiState.settings.isAutoTunnelEnabled && !uiState.settings.isAutoTunnelPaused) { + appViewModel.showSnackbarMessage( + context.getString(R.string.turn_off_auto), + ) + } else { + selectedTunnel = tunnel + navController.navigate( + "${Screen.Option.route}/${selectedTunnel?.id}", + ) + } + }, + ) { + val icon = Icons.Rounded.Settings + Icon( + icon, + icon.name, + ) + } + IconButton( + modifier = Modifier.focusRequester(focusRequester), + onClick = { + if ( + uiState.vpnState.status == TunnelState.UP && + (uiState.vpnState.tunnelConfig?.name == tunnel.name) + ) { + expanded.value = !expanded.value + } else { + appViewModel.showSnackbarMessage( + context.getString(R.string.turn_on_tunnel), + ) + } + }, + ) { + val icon = Icons.Rounded.Info + Icon(icon, icon.name) + } + IconButton( + onClick = { viewModel.onCopyTunnel(tunnel) }, + ) { + val icon = Icons.Rounded.CopyAll + Icon(icon, icon.name) + } + IconButton( + onClick = { + if ( + uiState.vpnState.status == TunnelState.UP && + tunnel.name == uiState.vpnState.tunnelConfig?.name + ) { + appViewModel.showSnackbarMessage( + context.getString(R.string.turn_off_tunnel), + ) + } else { + selectedTunnel = tunnel + showDeleteTunnelAlertDialog = true + } + }, + ) { + val icon = Icons.Rounded.Delete + Icon( + icon, + icon.name, + ) + } + TunnelSwitch() + } + } else { + TunnelSwitch() + } + } + }, + ) + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainUiState.kt index f9de5c8d..fe49c303 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainUiState.kt @@ -5,8 +5,8 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs data class MainUiState( - val settings: Settings = Settings(), - val tunnels: TunnelConfigs = emptyList(), - val vpnState: VpnState = VpnState(), - val loading: Boolean = true + val settings: Settings = Settings(), + val tunnels: TunnelConfigs = emptyList(), + val vpnState: VpnState = VpnState(), + val loading: Boolean = true, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt index 46be06af..c4f0fbf9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt @@ -34,358 +34,356 @@ import javax.inject.Inject class MainViewModel @Inject constructor( - private val appDataRepository: AppDataRepository, - private val serviceManager: ServiceManager, - val vpnService: VpnService, - @IoDispatcher private val ioDispatcher: CoroutineDispatcher + private val appDataRepository: AppDataRepository, + private val serviceManager: ServiceManager, + val vpnService: VpnService, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, ) : ViewModel() { + val uiState = + combine( + appDataRepository.settings.getSettingsFlow(), + appDataRepository.tunnels.getTunnelConfigsFlow(), + vpnService.vpnState, + ) { settings, tunnels, vpnState -> + MainUiState(settings, tunnels, vpnState, false) + } + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), + MainUiState(), + ) - val uiState = - combine( - appDataRepository.settings.getSettingsFlow(), - appDataRepository.tunnels.getTunnelConfigsFlow(), - vpnService.vpnState, - ) { settings, tunnels, vpnState -> - MainUiState(settings, tunnels, vpnState, false) - } - .stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), - MainUiState(), - ) + private fun stopWatcherService(context: Context) { + serviceManager.stopWatcherService(context) + } - private fun stopWatcherService(context: Context) { - serviceManager.stopWatcherService(context) - } + fun onDelete(tunnel: TunnelConfig, context: Context) { + viewModelScope.launch { + val settings = appDataRepository.settings.getSettings() + val isPrimary = tunnel.isPrimaryTunnel + if (appDataRepository.tunnels.count() == 1 || isPrimary) { + stopWatcherService(context) + resetTunnelSetting(settings) + } + appDataRepository.tunnels.delete(tunnel) + WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate() + } + } - fun onDelete(tunnel: TunnelConfig, context: Context) { - viewModelScope.launch { - val settings = appDataRepository.settings.getSettings() - val isPrimary = tunnel.isPrimaryTunnel - if (appDataRepository.tunnels.count() == 1 || isPrimary) { - stopWatcherService(context) - resetTunnelSetting(settings) - } - appDataRepository.tunnels.delete(tunnel) - WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate() - } - } + private fun resetTunnelSetting(settings: Settings) { + saveSettings( + settings.copy( + isAutoTunnelEnabled = false, + isAlwaysOnVpnEnabled = false, + ), + ) + } - private fun resetTunnelSetting(settings: Settings) { - saveSettings( - settings.copy( - isAutoTunnelEnabled = false, - isAlwaysOnVpnEnabled = false, - ), - ) - } + fun onTunnelStart(tunnelConfig: TunnelConfig, context: Context) = viewModelScope.launch { + Timber.d("On start called!") + serviceManager.startVpnService( + context, + tunnelConfig.id, + isManualStart = true, + ) + } - fun onTunnelStart(tunnelConfig: TunnelConfig, context: Context) = - viewModelScope.launch { - Timber.d("On start called!") - serviceManager.startVpnService( - context, - tunnelConfig.id, - isManualStart = true, - ) - } + fun onTunnelStop(context: Context) = viewModelScope.launch { + Timber.i("Stopping active tunnel") + serviceManager.stopVpnService(context, isManualStop = true) + } + private fun validateConfigString(config: String, configType: ConfigType) { + when (configType) { + ConfigType.AMNEZIA -> TunnelConfig.configFromAmQuick(config) + ConfigType.WIREGUARD -> TunnelConfig.configFromWgQuick(config) + } + } - fun onTunnelStop(context: Context) = - viewModelScope.launch { - Timber.i("Stopping active tunnel") - serviceManager.stopVpnService(context, isManualStop = true) - } + private fun generateQrCodeDefaultName(config: String, configType: ConfigType): String { + return try { + when (configType) { + ConfigType.AMNEZIA -> { + TunnelConfig.configFromAmQuick(config).peers[0].endpoint.get().host + } - private fun validateConfigString(config: String, configType: ConfigType) { - when (configType) { - ConfigType.AMNEZIA -> TunnelConfig.configFromAmQuick(config) - ConfigType.WIREGUARD -> TunnelConfig.configFromWgQuick(config) - } - } + ConfigType.WIREGUARD -> { + TunnelConfig.configFromWgQuick(config).peers[0].endpoint.get().host + } + } + } catch (e: Exception) { + Timber.e(e) + NumberUtils.generateRandomTunnelName() + } + } - private fun generateQrCodeDefaultName(config: String, configType: ConfigType): String { - return try { - when (configType) { - ConfigType.AMNEZIA -> { - TunnelConfig.configFromAmQuick(config).peers[0].endpoint.get().host - } + private fun generateQrCodeTunnelName(config: String, configType: ConfigType): String { + var defaultName = generateQrCodeDefaultName(config, configType) + val lines = config.lines().toMutableList() + val linesIterator = lines.iterator() + while (linesIterator.hasNext()) { + val next = linesIterator.next() + if (next.contains(Constants.QR_CODE_NAME_PROPERTY)) { + defaultName = next.substringAfter(Constants.QR_CODE_NAME_PROPERTY).trim() + break + } + } + return defaultName + } - ConfigType.WIREGUARD -> { - TunnelConfig.configFromWgQuick(config).peers[0].endpoint.get().host - } - } - } catch (e: Exception) { - Timber.e(e) - NumberUtils.generateRandomTunnelName() - } - } + suspend fun onTunnelQrResult(result: String, configType: ConfigType): Result { + return withContext(ioDispatcher) { + try { + validateConfigString(result, configType) + val tunnelName = + makeTunnelNameUnique(generateQrCodeTunnelName(result, configType)) + val tunnelConfig = + when (configType) { + ConfigType.AMNEZIA -> { + TunnelConfig( + name = tunnelName, + amQuick = result, + wgQuick = + TunnelConfig.configFromAmQuick( + result, + ).toWgQuickString(), + ) + } - private fun generateQrCodeTunnelName(config: String, configType: ConfigType): String { - var defaultName = generateQrCodeDefaultName(config, configType) - val lines = config.lines().toMutableList() - val linesIterator = lines.iterator() - while (linesIterator.hasNext()) { - val next = linesIterator.next() - if (next.contains(Constants.QR_CODE_NAME_PROPERTY)) { - defaultName = next.substringAfter(Constants.QR_CODE_NAME_PROPERTY).trim() - break - } - } - return defaultName - } + ConfigType.WIREGUARD -> + TunnelConfig( + name = tunnelName, + wgQuick = result, + ) + } + addTunnel(tunnelConfig) + Result.success(Unit) + } catch (e: Exception) { + Timber.e(e) + Result.failure(WgTunnelExceptions.InvalidQrCode()) + } + } + } - suspend fun onTunnelQrResult(result: String, configType: ConfigType): Result { - return withContext(ioDispatcher) { - try { - validateConfigString(result, configType) - val tunnelName = makeTunnelNameUnique(generateQrCodeTunnelName(result, configType)) - val tunnelConfig = when (configType) { - ConfigType.AMNEZIA -> { - TunnelConfig( - name = tunnelName, amQuick = result, - wgQuick = TunnelConfig.configFromAmQuick(result).toWgQuickString(), - ) - } + private suspend fun makeTunnelNameUnique(name: String): String { + return withContext(ioDispatcher) { + val tunnels = appDataRepository.tunnels.getAll() + var tunnelName = name + var num = 1 + while (tunnels.any { it.name == tunnelName }) { + tunnelName = name + "($num)" + num++ + } + tunnelName + } + } - ConfigType.WIREGUARD -> TunnelConfig(name = tunnelName, wgQuick = result) - } - addTunnel(tunnelConfig) - Result.success(Unit) - } catch (e: Exception) { - Timber.e(e) - Result.failure(WgTunnelExceptions.InvalidQrCode()) - } - } - } + private fun saveTunnelConfigFromStream(stream: InputStream, fileName: String, type: ConfigType) { + var amQuick: String? = null + val wgQuick = + stream.use { + when (type) { + ConfigType.AMNEZIA -> { + val config = org.amnezia.awg.config.Config.parse(it) + amQuick = config.toAwgQuickString() + config.toWgQuickString() + } - private suspend fun makeTunnelNameUnique(name: String): String { - return withContext(ioDispatcher) { - val tunnels = appDataRepository.tunnels.getAll() - var tunnelName = name - var num = 1 - while (tunnels.any { it.name == tunnelName }) { - tunnelName = name + "(${num})" - num++ - } - tunnelName - } - } + ConfigType.WIREGUARD -> { + Config.parse(it).toWgQuickString() + } + } + } + viewModelScope.launch { + val tunnelName = makeTunnelNameUnique(getNameFromFileName(fileName)) + addTunnel( + TunnelConfig( + name = tunnelName, + wgQuick = wgQuick, + amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT, + ), + ) + } + } - private fun saveTunnelConfigFromStream( - stream: InputStream, - fileName: String, - type: ConfigType - ) { - var amQuick: String? = null - val wgQuick = stream.use { - when (type) { - ConfigType.AMNEZIA -> { - val config = org.amnezia.awg.config.Config.parse(it) - amQuick = config.toAwgQuickString() - config.toWgQuickString() - } + private fun getInputStreamFromUri(uri: Uri, context: Context): InputStream? { + return context.applicationContext.contentResolver.openInputStream(uri) + } - ConfigType.WIREGUARD -> { - Config.parse(it).toWgQuickString() - } - } - } - viewModelScope.launch { - val tunnelName = makeTunnelNameUnique(getNameFromFileName(fileName)) - addTunnel( - TunnelConfig( - name = tunnelName, - wgQuick = wgQuick, - amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT, - ), - ) - } - } + suspend fun onTunnelFileSelected(uri: Uri, configType: ConfigType, context: Context): Result { + return withContext(ioDispatcher) { + try { + if (isValidUriContentScheme(uri)) { + val fileName = getFileName(context, uri) + return@withContext when (getFileExtensionFromFileName(fileName)) { + Constants.CONF_FILE_EXTENSION -> + saveTunnelFromConfUri(fileName, uri, configType, context) - private fun getInputStreamFromUri(uri: Uri, context: Context): InputStream? { - return context.applicationContext.contentResolver.openInputStream(uri) - } + Constants.ZIP_FILE_EXTENSION -> + saveTunnelsFromZipUri( + uri, + configType, + context, + ) - suspend fun onTunnelFileSelected( - uri: Uri, - configType: ConfigType, - context: Context - ): Result { - return withContext(ioDispatcher) { - try { - if (isValidUriContentScheme(uri)) { - val fileName = getFileName(context, uri) - return@withContext when (getFileExtensionFromFileName(fileName)) { - Constants.CONF_FILE_EXTENSION -> - saveTunnelFromConfUri(fileName, uri, configType, context) + else -> Result.failure(WgTunnelExceptions.InvalidFileExtension()) + } + } else { + Result.failure(WgTunnelExceptions.InvalidFileExtension()) + } + } catch (e: Exception) { + Timber.e(e) + Result.failure(WgTunnelExceptions.FileReadFailed()) + } + } + } - Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri( - uri, - configType, - context, - ) + private suspend fun saveTunnelsFromZipUri(uri: Uri, configType: ConfigType, context: Context): Result { + return withContext(ioDispatcher) { + ZipInputStream(getInputStreamFromUri(uri, context)).use { zip -> + generateSequence { zip.nextEntry } + .filterNot { + it.isDirectory || + getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION + } + .forEach { + val name = getNameFromFileName(it.name) + withContext(viewModelScope.coroutineContext) { + try { + var amQuick: String? = null + val wgQuick = + when (configType) { + ConfigType.AMNEZIA -> { + val config = + org.amnezia.awg.config.Config.parse( + zip, + ) + amQuick = config.toAwgQuickString() + config.toWgQuickString() + } - else -> Result.failure(WgTunnelExceptions.InvalidFileExtension()) - } - } else { - Result.failure(WgTunnelExceptions.InvalidFileExtension()) - } - } catch (e: Exception) { - Timber.e(e) - Result.failure(WgTunnelExceptions.FileReadFailed()) - } - } - } + ConfigType.WIREGUARD -> { + Config.parse(zip).toWgQuickString() + } + } + addTunnel( + TunnelConfig( + name = makeTunnelNameUnique(name), + wgQuick = wgQuick, + amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT, + ), + ) + Result.success(Unit) + } catch (e: Exception) { + Result.failure(WgTunnelExceptions.FileReadFailed()) + } + } + } + Result.success(Unit) + } + } + } - private suspend fun saveTunnelsFromZipUri( - uri: Uri, - configType: ConfigType, - context: Context - ): Result { - return withContext(ioDispatcher) { - ZipInputStream(getInputStreamFromUri(uri, context)).use { zip -> - generateSequence { zip.nextEntry } - .filterNot { - it.isDirectory || - getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION - } - .forEach { - val name = getNameFromFileName(it.name) - withContext(viewModelScope.coroutineContext) { - try { - var amQuick: String? = null - val wgQuick = - when (configType) { - ConfigType.AMNEZIA -> { - val config = org.amnezia.awg.config.Config.parse(zip) - amQuick = config.toAwgQuickString() - config.toWgQuickString() - } + private suspend fun saveTunnelFromConfUri(name: String, uri: Uri, configType: ConfigType, context: Context): Result { + return withContext(ioDispatcher) { + val stream = getInputStreamFromUri(uri, context) + return@withContext if (stream != null) { + try { + saveTunnelConfigFromStream(stream, name, configType) + } catch (e: Exception) { + return@withContext Result.failure(WgTunnelExceptions.ConfigParseError()) + } + Result.success(Unit) + } else { + Result.failure(WgTunnelExceptions.FileReadFailed()) + } + } + } - ConfigType.WIREGUARD -> { - Config.parse(zip).toWgQuickString() - } - } - addTunnel( - TunnelConfig( - name = makeTunnelNameUnique(name), - wgQuick = wgQuick, - amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT, - ), - ) - Result.success(Unit) - } catch (e: Exception) { - Result.failure(WgTunnelExceptions.FileReadFailed()) - } - } - } - Result.success(Unit) - } - } - } + private fun addTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch { + val firstTunnel = appDataRepository.tunnels.count() == 0 + saveTunnel(tunnelConfig) + if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate() + } - private suspend fun saveTunnelFromConfUri( - name: String, - uri: Uri, - configType: ConfigType, - context: Context - ): Result { - return withContext(ioDispatcher) { - val stream = getInputStreamFromUri(uri, context) - return@withContext if (stream != null) { - try { - saveTunnelConfigFromStream(stream, name, configType) - } catch (e: Exception) { - return@withContext Result.failure(WgTunnelExceptions.ConfigParseError()) - } - Result.success(Unit) - } else { - Result.failure(WgTunnelExceptions.FileReadFailed()) - } - } - } + fun pauseAutoTunneling() = viewModelScope.launch { + appDataRepository.settings.save( + uiState.value.settings.copy(isAutoTunnelPaused = true), + ) + WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate() + } - private fun addTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch { - val firstTunnel = appDataRepository.tunnels.count() == 0 - saveTunnel(tunnelConfig) - if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate() - } + fun resumeAutoTunneling() = viewModelScope.launch { + appDataRepository.settings.save( + uiState.value.settings.copy(isAutoTunnelPaused = false), + ) + WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate() + } - fun pauseAutoTunneling() = - viewModelScope.launch { - appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = true)) - WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate() - } + private fun saveTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch { + appDataRepository.tunnels.save(tunnelConfig) + } - fun resumeAutoTunneling() = - viewModelScope.launch { - appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = false)) - WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate() - } + private fun getFileNameByCursor(context: Context, uri: Uri): String? { + context.contentResolver.query(uri, null, null, null, null)?.use { + return getDisplayNameByCursor(it) + } + return null + } - private fun saveTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch { - appDataRepository.tunnels.save(tunnelConfig) - } + private fun getDisplayNameColumnIndex(cursor: Cursor): Int? { + val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + return if (columnIndex != -1) { + return columnIndex + } else { + null + } + } - private fun getFileNameByCursor(context: Context, uri: Uri): String? { - context.contentResolver.query(uri, null, null, null, null)?.use { - return getDisplayNameByCursor(it) - } - return null - } + private fun getDisplayNameByCursor(cursor: Cursor): String? { + return if (cursor.moveToFirst()) { + val index = getDisplayNameColumnIndex(cursor) + if (index != null) { + cursor.getString(index) + } else { + null + } + } else { + null + } + } - private fun getDisplayNameColumnIndex(cursor: Cursor): Int? { - val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) - return if (columnIndex != -1) { - return columnIndex - } else { - null - } - } + private fun isValidUriContentScheme(uri: Uri): Boolean { + return uri.scheme == Constants.URI_CONTENT_SCHEME + } - private fun getDisplayNameByCursor(cursor: Cursor): String? { - return if (cursor.moveToFirst()) { - val index = getDisplayNameColumnIndex(cursor) - if (index != null) { - cursor.getString(index) - } else null - } else null - } + private fun getFileName(context: Context, uri: Uri): String { + return getFileNameByCursor(context, uri) ?: NumberUtils.generateRandomTunnelName() + } - private fun isValidUriContentScheme(uri: Uri): Boolean { - return uri.scheme == Constants.URI_CONTENT_SCHEME - } + private fun getNameFromFileName(fileName: String): String { + return fileName.substring(0, fileName.lastIndexOf('.')) + } - private fun getFileName(context: Context, uri: Uri): String { - return getFileNameByCursor(context, uri) ?: NumberUtils.generateRandomTunnelName() - } + private fun getFileExtensionFromFileName(fileName: String): String? { + return try { + fileName.substring(fileName.lastIndexOf('.')) + } catch (e: Exception) { + Timber.e(e) + null + } + } - private fun getNameFromFileName(fileName: String): String { - return fileName.substring(0, fileName.lastIndexOf('.')) - } + private fun saveSettings(settings: Settings) = viewModelScope.launch { appDataRepository.settings.save(settings) } - private fun getFileExtensionFromFileName(fileName: String): String? { - return try { - fileName.substring(fileName.lastIndexOf('.')) - } catch (e: Exception) { - Timber.e(e) - null - } - } - - private fun saveSettings(settings: Settings) = - viewModelScope.launch { appDataRepository.settings.save(settings) } - - - fun onCopyTunnel(tunnel: TunnelConfig?) = viewModelScope.launch { - tunnel?.let { - saveTunnel( - TunnelConfig( - name = it.name.plus(NumberUtils.randomThree()), - wgQuick = it.wgQuick, - ), - ) - } - } + fun onCopyTunnel(tunnel: TunnelConfig?) = viewModelScope.launch { + tunnel?.let { + saveTunnel( + TunnelConfig( + name = it.name.plus(NumberUtils.randomThree()), + wgQuick = it.wgQuick, + ), + ) + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt index e7a66e9a..a0299fab 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt @@ -77,283 +77,295 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalLayoutApi::class) @Composable fun OptionsScreen( - optionsViewModel: OptionsViewModel = hiltViewModel(), - navController: NavController, - appViewModel: AppViewModel, - focusRequester: FocusRequester, - tunnelId: String + optionsViewModel: OptionsViewModel = hiltViewModel(), + navController: NavController, + appViewModel: AppViewModel, + focusRequester: FocusRequester, + tunnelId: String, ) { - val scrollState = rememberScrollState() - val uiState by optionsViewModel.uiState.collectAsStateWithLifecycle() - val context = LocalContext.current + val scrollState = rememberScrollState() + val uiState by optionsViewModel.uiState.collectAsStateWithLifecycle() + val context = LocalContext.current - val interactionSource = remember { MutableInteractionSource() } - val scope = rememberCoroutineScope() - val focusManager = LocalFocusManager.current - val screenPadding = 5.dp - val fillMaxWidth = .85f + val interactionSource = remember { MutableInteractionSource() } + val scope = rememberCoroutineScope() + val focusManager = LocalFocusManager.current + val screenPadding = 5.dp + val fillMaxWidth = .85f - var currentText by remember { mutableStateOf("") } + var currentText by remember { mutableStateOf("") } - LaunchedEffect(Unit) { - optionsViewModel.init(tunnelId) - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - delay(Constants.FOCUS_REQUEST_DELAY) - focusRequester.requestFocus() - } - } + LaunchedEffect(Unit) { + optionsViewModel.init(tunnelId) + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + delay(Constants.FOCUS_REQUEST_DELAY) + focusRequester.requestFocus() + } + } - fun saveTrustedSSID() { - if (currentText.isNotEmpty()) { - scope.launch { - optionsViewModel.onSaveRunSSID(currentText).onSuccess { - currentText = "" - }.onFailure { - appViewModel.showSnackbarMessage(it.getMessage(context)) - } - } - } - } + fun saveTrustedSSID() { + if (currentText.isNotEmpty()) { + scope.launch { + optionsViewModel.onSaveRunSSID(currentText).onSuccess { + currentText = "" + }.onFailure { + appViewModel.showSnackbarMessage(it.getMessage(context)) + } + } + } + } - Scaffold( - floatingActionButton = { - val secondaryColor = MaterialTheme.colorScheme.secondary - val tvFobColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) - val fobColor = - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) tvFobColor else secondaryColor - val fobIconColor = - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) Color.White else MaterialTheme.colorScheme.background - AnimatedVisibility( - visible = true, - enter = slideInVertically(initialOffsetY = { it * 2 }), - exit = slideOutVertically(targetOffsetY = { it * 2 }), - modifier = Modifier - .focusRequester(focusRequester) - .focusGroup(), - ) { - MultiFloatingActionButton( - fabIcon = FabIcon( - iconRes = R.drawable.edit, - iconResAfterRotate = R.drawable.close, - iconRotate = 180f, - ), - fabOption = FabOption( - iconTint = fobIconColor, - backgroundTint = fobColor, - ), - itemsMultiFab = listOf( - MultiFabItem( - label = { - Text( - stringResource(id = R.string.amnezia), - color = Color.White, - textAlign = TextAlign.Center, - modifier = Modifier.padding(end = 10.dp), - ) - }, - modifier = Modifier - .size(40.dp), - icon = R.drawable.edit, - value = ConfigType.AMNEZIA.name, - miniFabOption = FabOption( - backgroundTint = fobColor, - fobIconColor, - ), - ), - MultiFabItem( - label = { - Text( - stringResource(id = R.string.wireguard), - color = Color.White, - textAlign = TextAlign.Center, - modifier = Modifier.padding(end = 10.dp), - ) - }, - icon = R.drawable.edit, - value = ConfigType.WIREGUARD.name, - miniFabOption = FabOption( - backgroundTint = fobColor, - fobIconColor, - ), - ), - ), - onFabItemClicked = { - val configType = ConfigType.valueOf(it.value) - navController.navigate( - "${Screen.Config.route}/${tunnelId}?configType=${configType.name}", - ) - }, - shape = RoundedCornerShape(16.dp), - ) - } - }, - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top, - modifier = - Modifier - .fillMaxSize() - .verticalScroll(scrollState) - .clickable( - indication = null, - interactionSource = interactionSource, - ) { - focusManager.clearFocus() - }, - ) { - Surface( - tonalElevation = 2.dp, - shadowElevation = 2.dp, - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surface, - modifier = - (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - Modifier - .height(IntrinsicSize.Min) - .fillMaxWidth(fillMaxWidth) - .padding(top = 10.dp) - } else { - Modifier - .fillMaxWidth(fillMaxWidth) - .padding(top = 20.dp) - }) - .padding(bottom = 10.dp), - ) { - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Top, - modifier = Modifier.padding(15.dp), - ) { - SectionTitle( - title = stringResource(id = R.string.general), - padding = screenPadding, - ) - ConfigurationToggle( - stringResource(R.string.set_primary_tunnel), - enabled = true, - checked = uiState.isDefaultTunnel, - modifier = Modifier - .focusRequester(focusRequester), - padding = screenPadding, - onCheckChanged = { optionsViewModel.onTogglePrimaryTunnel() }, - ) - } - } - Surface( - tonalElevation = 2.dp, - shadowElevation = 2.dp, - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surface, - modifier = - (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - Modifier - .height(IntrinsicSize.Min) - .fillMaxWidth(fillMaxWidth) - .padding(top = 10.dp) - } else { - Modifier - .fillMaxWidth(fillMaxWidth) - .padding(top = 20.dp) - }) - .padding(bottom = 10.dp), - ) { - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Top, - modifier = Modifier.padding(15.dp), - ) { - SectionTitle( - title = stringResource(id = R.string.auto_tunneling), - padding = screenPadding, - ) - ConfigurationToggle( - stringResource(R.string.mobile_data_tunnel), - enabled = true, - checked = uiState.tunnel?.isMobileDataTunnel == true, - padding = screenPadding, - onCheckChanged = { optionsViewModel.onToggleIsMobileDataTunnel() }, - ) - Column { - FlowRow( - modifier = Modifier - .padding(screenPadding) - .fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(5.dp), - ) { - uiState.tunnel?.tunnelNetworks?.forEach { ssid -> - ClickableIconButton( - onClick = { - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - focusRequester.requestFocus() - optionsViewModel.onDeleteRunSSID(ssid) - } - }, - onIconClick = { - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus() - optionsViewModel.onDeleteRunSSID(ssid) - - }, - text = ssid, - icon = Icons.Filled.Close, - enabled = true, - ) - } - if (uiState.tunnel == null || uiState.tunnel?.tunnelNetworks?.isEmpty() == true) { - Text( - stringResource(R.string.no_wifi_names_configured), - fontStyle = FontStyle.Italic, - color = Color.Gray, - ) - } - } - OutlinedTextField( - enabled = true, - value = currentText, - onValueChange = { currentText = it }, - label = { Text(stringResource(id = R.string.use_tunnel_on_wifi_name)) }, - modifier = - Modifier - .padding( - start = screenPadding, - top = 5.dp, - bottom = 10.dp, - ), - maxLines = 1, - keyboardOptions = - KeyboardOptions( - capitalization = KeyboardCapitalization.None, - imeAction = ImeAction.Done, - ), - keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }), - trailingIcon = { - if (currentText != "") { - IconButton(onClick = { saveTrustedSSID() }) { - Icon( - imageVector = Icons.Outlined.Add, - contentDescription = - if (currentText == "") { - stringResource( - id = - R.string - .trusted_ssid_empty_description, - ) - } else { - stringResource( - id = - R.string - .trusted_ssid_value_description, - ) - }, - tint = MaterialTheme.colorScheme.primary, - ) - } - } - }, - ) - } - } - } - } - } + Scaffold( + floatingActionButton = { + val secondaryColor = MaterialTheme.colorScheme.secondary + val tvFobColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) + val fobColor = + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) tvFobColor else secondaryColor + val fobIconColor = + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) Color.White else MaterialTheme.colorScheme.background + AnimatedVisibility( + visible = true, + enter = slideInVertically(initialOffsetY = { it * 2 }), + exit = slideOutVertically(targetOffsetY = { it * 2 }), + modifier = + Modifier + .focusRequester(focusRequester) + .focusGroup(), + ) { + MultiFloatingActionButton( + fabIcon = + FabIcon( + iconRes = R.drawable.edit, + iconResAfterRotate = R.drawable.close, + iconRotate = 180f, + ), + fabOption = + FabOption( + iconTint = fobIconColor, + backgroundTint = fobColor, + ), + itemsMultiFab = + listOf( + MultiFabItem( + label = { + Text( + stringResource(id = R.string.amnezia), + color = Color.White, + textAlign = TextAlign.Center, + modifier = Modifier.padding(end = 10.dp), + ) + }, + modifier = + Modifier + .size(40.dp), + icon = R.drawable.edit, + value = ConfigType.AMNEZIA.name, + miniFabOption = + FabOption( + backgroundTint = fobColor, + fobIconColor, + ), + ), + MultiFabItem( + label = { + Text( + stringResource(id = R.string.wireguard), + color = Color.White, + textAlign = TextAlign.Center, + modifier = Modifier.padding(end = 10.dp), + ) + }, + icon = R.drawable.edit, + value = ConfigType.WIREGUARD.name, + miniFabOption = + FabOption( + backgroundTint = fobColor, + fobIconColor, + ), + ), + ), + onFabItemClicked = { + val configType = ConfigType.valueOf(it.value) + navController.navigate( + "${Screen.Config.route}/$tunnelId?configType=${configType.name}", + ) + }, + shape = RoundedCornerShape(16.dp), + ) + } + }, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + modifier = + Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .clickable( + indication = null, + interactionSource = interactionSource, + ) { + focusManager.clearFocus() + }, + ) { + Surface( + tonalElevation = 2.dp, + shadowElevation = 2.dp, + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surface, + modifier = + ( + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + Modifier + .height(IntrinsicSize.Min) + .fillMaxWidth(fillMaxWidth) + .padding(top = 10.dp) + } else { + Modifier + .fillMaxWidth(fillMaxWidth) + .padding(top = 20.dp) + } + ) + .padding(bottom = 10.dp), + ) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier.padding(15.dp), + ) { + SectionTitle( + title = stringResource(id = R.string.general), + padding = screenPadding, + ) + ConfigurationToggle( + stringResource(R.string.set_primary_tunnel), + enabled = true, + checked = uiState.isDefaultTunnel, + modifier = + Modifier + .focusRequester(focusRequester), + padding = screenPadding, + onCheckChanged = { optionsViewModel.onTogglePrimaryTunnel() }, + ) + } + } + Surface( + tonalElevation = 2.dp, + shadowElevation = 2.dp, + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surface, + modifier = + ( + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + Modifier + .height(IntrinsicSize.Min) + .fillMaxWidth(fillMaxWidth) + .padding(top = 10.dp) + } else { + Modifier + .fillMaxWidth(fillMaxWidth) + .padding(top = 20.dp) + } + ) + .padding(bottom = 10.dp), + ) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier.padding(15.dp), + ) { + SectionTitle( + title = stringResource(id = R.string.auto_tunneling), + padding = screenPadding, + ) + ConfigurationToggle( + stringResource(R.string.mobile_data_tunnel), + enabled = true, + checked = uiState.tunnel?.isMobileDataTunnel == true, + padding = screenPadding, + onCheckChanged = { optionsViewModel.onToggleIsMobileDataTunnel() }, + ) + Column { + FlowRow( + modifier = + Modifier + .padding(screenPadding) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(5.dp), + ) { + uiState.tunnel?.tunnelNetworks?.forEach { ssid -> + ClickableIconButton( + onClick = { + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + focusRequester.requestFocus() + optionsViewModel.onDeleteRunSSID(ssid) + } + }, + onIconClick = { + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus() + optionsViewModel.onDeleteRunSSID(ssid) + }, + text = ssid, + icon = Icons.Filled.Close, + enabled = true, + ) + } + if (uiState.tunnel == null || uiState.tunnel?.tunnelNetworks?.isEmpty() == true) { + Text( + stringResource(R.string.no_wifi_names_configured), + fontStyle = FontStyle.Italic, + color = Color.Gray, + ) + } + } + OutlinedTextField( + enabled = true, + value = currentText, + onValueChange = { currentText = it }, + label = { Text(stringResource(id = R.string.use_tunnel_on_wifi_name)) }, + modifier = + Modifier + .padding( + start = screenPadding, + top = 5.dp, + bottom = 10.dp, + ), + maxLines = 1, + keyboardOptions = + KeyboardOptions( + capitalization = KeyboardCapitalization.None, + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }), + trailingIcon = { + if (currentText != "") { + IconButton(onClick = { saveTrustedSSID() }) { + Icon( + imageVector = Icons.Outlined.Add, + contentDescription = + if (currentText == "") { + stringResource( + id = + R.string + .trusted_ssid_empty_description, + ) + } else { + stringResource( + id = + R.string + .trusted_ssid_value_description, + ) + }, + tint = MaterialTheme.colorScheme.primary, + ) + } + } + }, + ) + } + } + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsUiState.kt index 828c4865..60f57ffa 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsUiState.kt @@ -3,7 +3,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.options import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig data class OptionsUiState( - val id: String? = null, - val tunnel: TunnelConfig? = null, - val isDefaultTunnel: Boolean = false + val id: String? = null, + val tunnel: TunnelConfig? = null, + val isDefaultTunnel: Boolean = false, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt index 6c5abde4..79a6d9db 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt @@ -19,84 +19,92 @@ import kotlinx.coroutines.withContext import javax.inject.Inject @HiltViewModel -class OptionsViewModel @Inject +class OptionsViewModel +@Inject constructor( - private val appDataRepository: AppDataRepository + private val appDataRepository: AppDataRepository, ) : ViewModel() { + private val _optionState = MutableStateFlow(OptionsUiState()) - private val _optionState = MutableStateFlow(OptionsUiState()) + val uiState = + combine( + appDataRepository.tunnels.getTunnelConfigsFlow(), + _optionState, + ) { tunnels, optionState -> + if (optionState.id != null) { + val tunnelConfig = tunnels.fastFirstOrNull { it.id.toString() == optionState.id } + val isPrimaryTunnel = tunnelConfig?.isPrimaryTunnel == true + OptionsUiState(optionState.id, tunnelConfig, isPrimaryTunnel) + } else { + OptionsUiState() + } + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), + OptionsUiState(), + ) - val uiState = combine( - appDataRepository.tunnels.getTunnelConfigsFlow(), - _optionState, - ) { tunnels, optionState -> - if (optionState.id != null) { - val tunnelConfig = tunnels.fastFirstOrNull { it.id.toString() == optionState.id } - val isPrimaryTunnel = tunnelConfig?.isPrimaryTunnel == true - OptionsUiState(optionState.id, tunnelConfig, isPrimaryTunnel) - } else OptionsUiState() - }.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), - OptionsUiState(), - ) + fun init(tunnelId: String) { + _optionState.update { + it.copy( + id = tunnelId, + ) + } + } - fun init(tunnelId: String) { - _optionState.update { - it.copy( - id = tunnelId, - ) - } - } + fun onDeleteRunSSID(ssid: String) = viewModelScope.launch { + uiState.value.tunnel?.let { + appDataRepository.tunnels.save( + tunnelConfig = + it.copy( + tunnelNetworks = (uiState.value.tunnel!!.tunnelNetworks - ssid).toMutableList(), + ), + ) + } + } - fun onDeleteRunSSID(ssid: String) = viewModelScope.launch { - uiState.value.tunnel?.let { - appDataRepository.tunnels.save( - tunnelConfig = it.copy( - tunnelNetworks = (uiState.value.tunnel!!.tunnelNetworks - ssid).toMutableList(), - ), - ) - } - } + private fun saveTunnel(tunnelConfig: TunnelConfig?) = viewModelScope.launch { + tunnelConfig?.let { + appDataRepository.tunnels.save(it) + } + } - private fun saveTunnel(tunnelConfig: TunnelConfig?) = viewModelScope.launch { - tunnelConfig?.let { - appDataRepository.tunnels.save(it) - } - } + suspend fun onSaveRunSSID(ssid: String): Result { + val trimmed = ssid.trim() + val tunnelsWithName = + withContext(viewModelScope.coroutineContext) { + appDataRepository.tunnels.findByTunnelNetworksName(trimmed) + } + return if (uiState.value.tunnel?.tunnelNetworks?.contains(trimmed) != true && + tunnelsWithName.isEmpty() + ) { + uiState.value.tunnel?.tunnelNetworks?.add(trimmed) + saveTunnel(uiState.value.tunnel) + Result.success(Unit) + } else { + Result.failure(WgTunnelExceptions.SsidConflict()) + } + } - suspend fun onSaveRunSSID(ssid: String): Result { - val trimmed = ssid.trim() - val tunnelsWithName = withContext(viewModelScope.coroutineContext) { - appDataRepository.tunnels.findByTunnelNetworksName(trimmed) - } - return if (uiState.value.tunnel?.tunnelNetworks?.contains(trimmed) != true && - tunnelsWithName.isEmpty()) { - uiState.value.tunnel?.tunnelNetworks?.add(trimmed) - saveTunnel(uiState.value.tunnel) - Result.success(Unit) - } else { - Result.failure(WgTunnelExceptions.SsidConflict()) - } - } + fun onToggleIsMobileDataTunnel() = viewModelScope.launch { + uiState.value.tunnel?.let { + if (it.isMobileDataTunnel) { + appDataRepository.tunnels.updateMobileDataTunnel(null) + } else { + appDataRepository.tunnels.updateMobileDataTunnel(it) + } + } + } - fun onToggleIsMobileDataTunnel() = viewModelScope.launch { - uiState.value.tunnel?.let { - if (it.isMobileDataTunnel) { - appDataRepository.tunnels.updateMobileDataTunnel(null) - } else appDataRepository.tunnels.updateMobileDataTunnel(it) - } - } - - fun onTogglePrimaryTunnel() = viewModelScope.launch { - if (uiState.value.tunnel != null) { - appDataRepository.tunnels.updatePrimaryTunnel( - when (uiState.value.isDefaultTunnel) { - true -> null - false -> uiState.value.tunnel - }, - ) - WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate() - } - } + fun onTogglePrimaryTunnel() = viewModelScope.launch { + if (uiState.value.tunnel != null) { + appDataRepository.tunnels.updatePrimaryTunnel( + when (uiState.value.isDefaultTunnel) { + true -> null + false -> uiState.value.tunnel + }, + ) + WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate() + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/pinlock/PinLockScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/pinlock/PinLockScreen.kt index 1e51da3f..6a49483d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/pinlock/PinLockScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/pinlock/PinLockScreen.kt @@ -15,39 +15,43 @@ import xyz.teamgravity.pin_lock_compose.PinLock @Composable fun PinLockScreen(navController: NavController, appViewModel: AppViewModel) { - val context = LocalContext.current - PinLock( - title = { pinExists -> - Text( - text = if (pinExists) stringResource(id = R.string.enter_pin) else stringResource( - id = R.string.create_pin, - ), - ) - }, - color = MaterialTheme.colorScheme.surface, - onPinCorrect = { - // pin is correct, navigate or hide pin lock - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - navController.navigate(Screen.Main.route) - } else { - val isPopped = navController.popBackStack() - if (!isPopped) { - navController.navigate(Screen.Main.route) - } - } - - }, - onPinIncorrect = { - // pin is incorrect, show error - appViewModel.showSnackbarMessage( - StringValue.StringResource(R.string.incorrect_pin).asString(context), - ) - }, - onPinCreated = { - // pin created for the first time, navigate or hide pin lock - appViewModel.showSnackbarMessage( - StringValue.StringResource(R.string.pin_created).asString(context), - ) - }, - ) + val context = LocalContext.current + PinLock( + title = { pinExists -> + Text( + text = + if (pinExists) { + stringResource(id = R.string.enter_pin) + } else { + stringResource( + id = R.string.create_pin, + ) + }, + ) + }, + color = MaterialTheme.colorScheme.surface, + onPinCorrect = { + // pin is correct, navigate or hide pin lock + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + navController.navigate(Screen.Main.route) + } else { + val isPopped = navController.popBackStack() + if (!isPopped) { + navController.navigate(Screen.Main.route) + } + } + }, + onPinIncorrect = { + // pin is incorrect, show error + appViewModel.showSnackbarMessage( + StringValue.StringResource(R.string.incorrect_pin).asString(context), + ) + }, + onPinCreated = { + // pin created for the first time, navigate or hide pin lock + appViewModel.showSnackbarMessage( + StringValue.StringResource(R.string.pin_created).asString(context), + ) + }, + ) } 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 34be7742..8e7ac4ff 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 @@ -87,610 +87,654 @@ import timber.log.Timber import java.io.File @OptIn( - ExperimentalPermissionsApi::class, - ExperimentalLayoutApi::class, + ExperimentalPermissionsApi::class, + ExperimentalLayoutApi::class, ) @Composable fun SettingsScreen( - viewModel: SettingsViewModel = hiltViewModel(), - appViewModel: AppViewModel, - navController: NavController, - focusRequester: FocusRequester, + viewModel: SettingsViewModel = hiltViewModel(), + appViewModel: AppViewModel, + navController: NavController, + focusRequester: FocusRequester, ) { - val context = LocalContext.current - val focusManager = LocalFocusManager.current - val scope = rememberCoroutineScope() - val scrollState = rememberScrollState() - val interactionSource = remember { MutableInteractionSource() } + val context = LocalContext.current + val focusManager = LocalFocusManager.current + val scope = rememberCoroutineScope() + val scrollState = rememberScrollState() + val interactionSource = remember { MutableInteractionSource() } - val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val kernelSupport by viewModel.kernelSupport.collectAsStateWithLifecycle() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val kernelSupport by viewModel.kernelSupport.collectAsStateWithLifecycle() - val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION) - var currentText by remember { mutableStateOf("") } - var isBackgroundLocationGranted by remember { mutableStateOf(true) } - var showLocationServicesAlertDialog by remember { mutableStateOf(false) } - var didExportFiles by remember { mutableStateOf(false) } - var showAuthPrompt by remember { mutableStateOf(false) } + val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION) + var currentText by remember { mutableStateOf("") } + var isBackgroundLocationGranted by remember { mutableStateOf(true) } + var showLocationServicesAlertDialog by remember { mutableStateOf(false) } + var didExportFiles by remember { mutableStateOf(false) } + var showAuthPrompt by remember { mutableStateOf(false) } - val screenPadding = 5.dp - val fillMaxWidth = .85f + val screenPadding = 5.dp + val fillMaxWidth = .85f - LaunchedEffect(Unit) { - viewModel.checkKernelSupport() - } + LaunchedEffect(Unit) { + viewModel.checkKernelSupport() + } - val startForResult = - rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> - if (result.resultCode == Activity.RESULT_OK) { - result.data - // Handle the Intent - } - viewModel.setBatteryOptimizeDisableShown() - } + val startForResult = + rememberLauncherForActivityResult( + ActivityResultContracts.StartActivityForResult(), + ) { result: ActivityResult -> + if (result.resultCode == Activity.RESULT_OK) { + result.data + // Handle the Intent + } + viewModel.setBatteryOptimizeDisableShown() + } - fun exportAllConfigs() { - try { - val wgFiles = uiState.tunnels.map { config -> - val file = File(context.cacheDir, "${config.name}-wg.conf") - file.outputStream().use { - it.write(config.wgQuick.toByteArray()) - } - file - } - val amFiles = uiState.tunnels.mapNotNull { config -> - if (config.amQuick != TunnelConfig.AM_QUICK_DEFAULT) { - val file = File(context.cacheDir, "${config.name}-am.conf") - file.outputStream().use { - it.write(config.amQuick.toByteArray()) - } - file - } else null - } - scope.launch { - viewModel.onExportTunnels(wgFiles + amFiles).onFailure { - appViewModel.showSnackbarMessage(it.getMessage(context)) - }.onSuccess { - didExportFiles = true - appViewModel.showSnackbarMessage(context.getString(R.string.exported_configs_message)) - } - } - } catch (e: Exception) { - Timber.e(e) - } - } + fun exportAllConfigs() { + try { + val wgFiles = + uiState.tunnels.map { config -> + val file = File(context.cacheDir, "${config.name}-wg.conf") + file.outputStream().use { + it.write(config.wgQuick.toByteArray()) + } + file + } + val amFiles = + uiState.tunnels.mapNotNull { config -> + if (config.amQuick != TunnelConfig.AM_QUICK_DEFAULT) { + val file = File(context.cacheDir, "${config.name}-am.conf") + file.outputStream().use { + it.write(config.amQuick.toByteArray()) + } + file + } else { + null + } + } + scope.launch { + viewModel.onExportTunnels(wgFiles + amFiles).onFailure { + appViewModel.showSnackbarMessage(it.getMessage(context)) + }.onSuccess { + didExportFiles = true + appViewModel.showSnackbarMessage( + context.getString(R.string.exported_configs_message), + ) + } + } + } catch (e: Exception) { + Timber.e(e) + } + } - fun isBatteryOptimizationsDisabled(): Boolean { - val pm = context.getSystemService(POWER_SERVICE) as PowerManager - return pm.isIgnoringBatteryOptimizations(context.packageName) - } + fun isBatteryOptimizationsDisabled(): Boolean { + val pm = context.getSystemService(POWER_SERVICE) as PowerManager + return pm.isIgnoringBatteryOptimizations(context.packageName) + } - fun requestBatteryOptimizationsDisabled() { - val intent = - Intent().apply { - this.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS - data = Uri.fromParts("package", context.packageName, null) - } - startForResult.launch(intent) - } + fun requestBatteryOptimizationsDisabled() { + val intent = + Intent().apply { + this.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS + data = Uri.fromParts("package", context.packageName, null) + } + startForResult.launch(intent) + } - fun handleAutoTunnelToggle() { - if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) { - if (appViewModel.isRequiredPermissionGranted()) { - viewModel.onToggleAutoTunnel(context) - } - } else { - requestBatteryOptimizationsDisabled() - } - } + fun handleAutoTunnelToggle() { + if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) { + if (appViewModel.isRequiredPermissionGranted()) { + viewModel.onToggleAutoTunnel(context) + } + } else { + requestBatteryOptimizationsDisabled() + } + } - fun saveTrustedSSID() { - if (currentText.isNotEmpty()) { - viewModel.onSaveTrustedSSID(currentText).onSuccess { - currentText = "" - }.onFailure { - appViewModel.showSnackbarMessage(it.getMessage(context)) - } - } - } + fun saveTrustedSSID() { + if (currentText.isNotEmpty()) { + viewModel.onSaveTrustedSSID(currentText).onSuccess { + currentText = "" + }.onFailure { + appViewModel.showSnackbarMessage(it.getMessage(context)) + } + } + } - fun openSettings() { - val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS) - intentSettings.data = Uri.fromParts("package", context.packageName, null) - context.startActivity(intentSettings) - } + fun openSettings() { + val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS) + intentSettings.data = Uri.fromParts("package", context.packageName, null) + context.startActivity(intentSettings) + } - fun checkFineLocationGranted() { - isBackgroundLocationGranted = - if (!fineLocationState.status.isGranted) { - false - } else { - viewModel.setLocationDisclosureShown() - true - } - } + fun checkFineLocationGranted() { + isBackgroundLocationGranted = + if (!fineLocationState.status.isGranted) { + false + } else { + viewModel.setLocationDisclosureShown() + true + } + } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - if ( - WireGuardAutoTunnel.isRunningOnAndroidTv() && - Build.VERSION.SDK_INT == Build.VERSION_CODES.Q - ) { - checkFineLocationGranted() - } else { - val backgroundLocationState = - rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION) - isBackgroundLocationGranted = - if (!backgroundLocationState.status.isGranted) { - false - } else { - SideEffect { viewModel.setLocationDisclosureShown() } - true - } - } - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if ( + WireGuardAutoTunnel.isRunningOnAndroidTv() && + Build.VERSION.SDK_INT == Build.VERSION_CODES.Q + ) { + checkFineLocationGranted() + } else { + val backgroundLocationState = + rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION) + isBackgroundLocationGranted = + if (!backgroundLocationState.status.isGranted) { + false + } else { + SideEffect { viewModel.setLocationDisclosureShown() } + true + } + } + } - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { - checkFineLocationGranted() - } + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { + checkFineLocationGranted() + } - AnimatedVisibility(showLocationServicesAlertDialog) { - AlertDialog( - onDismissRequest = { showLocationServicesAlertDialog = false }, - confirmButton = { - TextButton( - onClick = { - showLocationServicesAlertDialog = false - handleAutoTunnelToggle() - }, - ) { - Text(text = stringResource(R.string.okay)) - } - }, - dismissButton = { - TextButton(onClick = { showLocationServicesAlertDialog = false }) { - 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)) }, - ) - } + AnimatedVisibility(showLocationServicesAlertDialog) { + AlertDialog( + onDismissRequest = { showLocationServicesAlertDialog = false }, + confirmButton = { + TextButton( + onClick = { + showLocationServicesAlertDialog = false + handleAutoTunnelToggle() + }, + ) { + Text(text = stringResource(R.string.okay)) + } + }, + dismissButton = { + TextButton(onClick = { showLocationServicesAlertDialog = false }) { + 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 (!uiState.isLocationDisclosureShown) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top, - modifier = Modifier - .fillMaxSize() - .verticalScroll(scrollState), - ) { - Icon( - Icons.Rounded.LocationOff, - contentDescription = stringResource(id = R.string.map), - modifier = Modifier - .padding(30.dp) - .size(128.dp), - ) - Text( - stringResource(R.string.prominent_background_location_title), - textAlign = TextAlign.Center, - modifier = Modifier.padding(30.dp), - fontSize = 20.sp, - ) - Text( - stringResource(R.string.prominent_background_location_message), - textAlign = TextAlign.Center, - modifier = Modifier.padding(30.dp), - fontSize = 15.sp, - ) - Row( - modifier = - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - Modifier - .fillMaxWidth() - .padding(10.dp) - } else { - Modifier - .fillMaxWidth() - .padding(30.dp) - }, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceEvenly, - ) { - TextButton(onClick = { viewModel.setLocationDisclosureShown() }) { - Text(stringResource(id = R.string.no_thanks)) - } - TextButton( - modifier = Modifier.focusRequester(focusRequester), - onClick = { - openSettings() - viewModel.setLocationDisclosureShown() - }, - ) { - Text(stringResource(id = R.string.turn_on)) - } - } - } - } + if (!uiState.isLocationDisclosureShown) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + modifier = + Modifier + .fillMaxSize() + .verticalScroll(scrollState), + ) { + Icon( + Icons.Rounded.LocationOff, + contentDescription = stringResource(id = R.string.map), + modifier = + Modifier + .padding(30.dp) + .size(128.dp), + ) + Text( + stringResource(R.string.prominent_background_location_title), + textAlign = TextAlign.Center, + modifier = Modifier.padding(30.dp), + fontSize = 20.sp, + ) + Text( + stringResource(R.string.prominent_background_location_message), + textAlign = TextAlign.Center, + modifier = Modifier.padding(30.dp), + fontSize = 15.sp, + ) + Row( + modifier = + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + Modifier + .fillMaxWidth() + .padding(10.dp) + } else { + Modifier + .fillMaxWidth() + .padding(30.dp) + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + TextButton(onClick = { viewModel.setLocationDisclosureShown() }) { + Text(stringResource(id = R.string.no_thanks)) + } + TextButton( + modifier = Modifier.focusRequester(focusRequester), + onClick = { + openSettings() + viewModel.setLocationDisclosureShown() + }, + ) { + Text(stringResource(id = R.string.turn_on)) + } + } + } + } - if (showAuthPrompt) { - AuthorizationPrompt( - onSuccess = { - showAuthPrompt = false - exportAllConfigs() - }, - onError = { _ -> - showAuthPrompt = false - appViewModel.showSnackbarMessage(context.getString(R.string.error_authentication_failed)) - }, - onFailure = { - showAuthPrompt = false - appViewModel.showSnackbarMessage(context.getString(R.string.error_authorization_failed)) - }, - ) - } + if (showAuthPrompt) { + AuthorizationPrompt( + onSuccess = { + showAuthPrompt = false + exportAllConfigs() + }, + onError = { _ -> + showAuthPrompt = false + appViewModel.showSnackbarMessage( + context.getString(R.string.error_authentication_failed), + ) + }, + onFailure = { + showAuthPrompt = false + appViewModel.showSnackbarMessage( + context.getString(R.string.error_authorization_failed), + ) + }, + ) + } - if (uiState.tunnels.isEmpty() && uiState.isLocationDisclosureShown) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxSize(), - ) { - Text( - stringResource(R.string.one_tunnel_required), - textAlign = TextAlign.Center, - modifier = Modifier.padding(15.dp), - fontStyle = FontStyle.Italic, - ) - } - } - if (uiState.isLocationDisclosureShown && uiState.tunnels.isNotEmpty()) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top, - modifier = - Modifier - .fillMaxSize() - .verticalScroll(scrollState) - .clickable( - indication = null, - interactionSource = interactionSource, - ) { - focusManager.clearFocus() - }, - ) { - Surface( - tonalElevation = 2.dp, - shadowElevation = 2.dp, - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surface, - modifier = - (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - Modifier - .height(IntrinsicSize.Min) - .fillMaxWidth(fillMaxWidth) - .padding(top = 10.dp) - } else { - Modifier - .fillMaxWidth(fillMaxWidth) - .padding(top = 20.dp) - }) - .padding(bottom = 10.dp), - ) { - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Top, - modifier = Modifier.padding(15.dp), - ) { - SectionTitle( - title = stringResource(id = R.string.auto_tunneling), - padding = screenPadding, - ) - ConfigurationToggle( - stringResource(id = R.string.tunnel_on_wifi), - enabled = - !(uiState.settings.isAutoTunnelEnabled || - uiState.settings.isAlwaysOnVpnEnabled), - checked = uiState.settings.isTunnelOnWifiEnabled, - padding = screenPadding, - onCheckChanged = { viewModel.onToggleTunnelOnWifi() }, - modifier = - if (uiState.settings.isAutoTunnelEnabled) Modifier - else - Modifier - .focusRequester(focusRequester), - ) - AnimatedVisibility(visible = uiState.settings.isTunnelOnWifiEnabled) { - Column { - FlowRow( - modifier = Modifier - .padding(screenPadding) - .fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(5.dp), - ) { - uiState.settings.trustedNetworkSSIDs.forEach { ssid -> - ClickableIconButton( - onClick = { - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - focusRequester.requestFocus() - viewModel.onDeleteTrustedSSID(ssid) - } - }, - onIconClick = { - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus() - viewModel.onDeleteTrustedSSID(ssid) + if (uiState.tunnels.isEmpty() && uiState.isLocationDisclosureShown) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxSize(), + ) { + Text( + stringResource(R.string.one_tunnel_required), + textAlign = TextAlign.Center, + modifier = Modifier.padding(15.dp), + fontStyle = FontStyle.Italic, + ) + } + } + if (uiState.isLocationDisclosureShown && uiState.tunnels.isNotEmpty()) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + modifier = + Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .clickable( + indication = null, + interactionSource = interactionSource, + ) { + focusManager.clearFocus() + }, + ) { + Surface( + tonalElevation = 2.dp, + shadowElevation = 2.dp, + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surface, + modifier = + ( + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + Modifier + .height(IntrinsicSize.Min) + .fillMaxWidth(fillMaxWidth) + .padding(top = 10.dp) + } else { + Modifier + .fillMaxWidth(fillMaxWidth) + .padding(top = 20.dp) + } + ) + .padding(bottom = 10.dp), + ) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier.padding(15.dp), + ) { + SectionTitle( + title = stringResource(id = R.string.auto_tunneling), + padding = screenPadding, + ) + ConfigurationToggle( + stringResource(id = R.string.tunnel_on_wifi), + enabled = + !( + uiState.settings.isAutoTunnelEnabled || + uiState.settings.isAlwaysOnVpnEnabled + ), + checked = uiState.settings.isTunnelOnWifiEnabled, + padding = screenPadding, + onCheckChanged = { viewModel.onToggleTunnelOnWifi() }, + modifier = + if (uiState.settings.isAutoTunnelEnabled) { + Modifier + } else { + Modifier + .focusRequester(focusRequester) + }, + ) + AnimatedVisibility(visible = uiState.settings.isTunnelOnWifiEnabled) { + Column { + FlowRow( + modifier = + Modifier + .padding(screenPadding) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(5.dp), + ) { + uiState.settings.trustedNetworkSSIDs.forEach { ssid -> + ClickableIconButton( + onClick = { + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + focusRequester.requestFocus() + viewModel.onDeleteTrustedSSID(ssid) + } + }, + onIconClick = { + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus() + viewModel.onDeleteTrustedSSID(ssid) + }, + text = ssid, + icon = Icons.Filled.Close, + enabled = + !( + uiState.settings.isAutoTunnelEnabled || + uiState.settings.isAlwaysOnVpnEnabled + ), + ) + } + if (uiState.settings.trustedNetworkSSIDs.isEmpty()) { + Text( + stringResource(R.string.none), + fontStyle = FontStyle.Italic, + color = Color.Gray, + ) + } + } + OutlinedTextField( + enabled = + !( + uiState.settings.isAutoTunnelEnabled || + uiState.settings.isAlwaysOnVpnEnabled + ), + value = currentText, + onValueChange = { currentText = it }, + label = { Text(stringResource(R.string.add_trusted_ssid)) }, + modifier = + Modifier + .padding( + start = screenPadding, + top = 5.dp, + bottom = 10.dp, + ), + maxLines = 1, + keyboardOptions = + KeyboardOptions( + capitalization = KeyboardCapitalization.None, + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }), + trailingIcon = { + if (currentText != "") { + IconButton(onClick = { saveTrustedSSID() }) { + Icon( + imageVector = Icons.Outlined.Add, + contentDescription = + if (currentText == "") { + stringResource( + id = + R.string + .trusted_ssid_empty_description, + ) + } else { + stringResource( + id = + R.string + .trusted_ssid_value_description, + ) + }, + tint = MaterialTheme.colorScheme.primary, + ) + } + } + }, + ) + } + } + ConfigurationToggle( + stringResource(R.string.tunnel_mobile_data), + enabled = + !( + uiState.settings.isAutoTunnelEnabled || + uiState.settings.isAlwaysOnVpnEnabled + ), + checked = uiState.settings.isTunnelOnMobileDataEnabled, + padding = screenPadding, + onCheckChanged = { viewModel.onToggleTunnelOnMobileData() }, + ) + ConfigurationToggle( + stringResource(id = R.string.tunnel_on_ethernet), + enabled = + !( + uiState.settings.isAutoTunnelEnabled || + uiState.settings.isAlwaysOnVpnEnabled + ), + checked = uiState.settings.isTunnelOnEthernetEnabled, + padding = screenPadding, + onCheckChanged = { viewModel.onToggleTunnelOnEthernet() }, + ) + ConfigurationToggle( + stringResource(R.string.restart_on_ping), + enabled = + !( + uiState.settings.isAutoTunnelEnabled || + uiState.settings.isAlwaysOnVpnEnabled + ), + checked = uiState.settings.isPingEnabled, + padding = screenPadding, + onCheckChanged = { viewModel.onToggleRestartOnPing() }, + ) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + ( + if (!uiState.settings.isAutoTunnelEnabled) { + Modifier + } else { + Modifier.focusRequester( + focusRequester, + ) + } + ) + .fillMaxSize() + .padding(top = 5.dp), + horizontalArrangement = Arrangement.Center, + ) { + TextButton( + enabled = !uiState.settings.isAlwaysOnVpnEnabled, + onClick = { + if ( + uiState.settings.isTunnelOnWifiEnabled && + !uiState.settings.isAutoTunnelEnabled + ) { + when (false) { + isBackgroundLocationGranted -> + appViewModel.showSnackbarMessage( + context.getString( + R.string.background_location_required, + ), + ) - }, - text = ssid, - icon = Icons.Filled.Close, - enabled = - !(uiState.settings.isAutoTunnelEnabled || - uiState.settings.isAlwaysOnVpnEnabled), - ) - } - if (uiState.settings.trustedNetworkSSIDs.isEmpty()) { - Text( - stringResource(R.string.none), - fontStyle = FontStyle.Italic, - color = Color.Gray, - ) - } - } - OutlinedTextField( - enabled = - !(uiState.settings.isAutoTunnelEnabled || - uiState.settings.isAlwaysOnVpnEnabled), - value = currentText, - onValueChange = { currentText = it }, - label = { Text(stringResource(R.string.add_trusted_ssid)) }, - modifier = - Modifier - .padding( - start = screenPadding, - top = 5.dp, - bottom = 10.dp, - ), - maxLines = 1, - keyboardOptions = - KeyboardOptions( - capitalization = KeyboardCapitalization.None, - imeAction = ImeAction.Done, - ), - keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }), - trailingIcon = { - if (currentText != "") { - IconButton(onClick = { saveTrustedSSID() }) { - Icon( - imageVector = Icons.Outlined.Add, - contentDescription = - if (currentText == "") { - stringResource( - id = - R.string - .trusted_ssid_empty_description, - ) - } else { - stringResource( - id = - R.string - .trusted_ssid_value_description, - ) - }, - tint = MaterialTheme.colorScheme.primary, - ) - } - } - }, - ) - } - } - ConfigurationToggle( - stringResource(R.string.tunnel_mobile_data), - enabled = - !(uiState.settings.isAutoTunnelEnabled || - uiState.settings.isAlwaysOnVpnEnabled), - checked = uiState.settings.isTunnelOnMobileDataEnabled, - padding = screenPadding, - onCheckChanged = { viewModel.onToggleTunnelOnMobileData() }, - ) - ConfigurationToggle( - stringResource(id = R.string.tunnel_on_ethernet), - enabled = - !(uiState.settings.isAutoTunnelEnabled || - uiState.settings.isAlwaysOnVpnEnabled), - checked = uiState.settings.isTunnelOnEthernetEnabled, - padding = screenPadding, - onCheckChanged = { viewModel.onToggleTunnelOnEthernet() }, - ) - ConfigurationToggle( - stringResource(R.string.restart_on_ping), - enabled = - !(uiState.settings.isAutoTunnelEnabled || - uiState.settings.isAlwaysOnVpnEnabled), - checked = uiState.settings.isPingEnabled, - padding = screenPadding, - onCheckChanged = { viewModel.onToggleRestartOnPing() }, - ) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = - (if (!uiState.settings.isAutoTunnelEnabled) Modifier - else - Modifier.focusRequester( - focusRequester, - )) - .fillMaxSize() - .padding(top = 5.dp), - horizontalArrangement = Arrangement.Center, - ) { - TextButton( - enabled = !uiState.settings.isAlwaysOnVpnEnabled, - onClick = { - if ( - uiState.settings.isTunnelOnWifiEnabled && - !uiState.settings.isAutoTunnelEnabled - ) { - when (false) { - isBackgroundLocationGranted -> - appViewModel.showSnackbarMessage( - context.getString(R.string.background_location_required), - ) + fineLocationState.status.isGranted -> + appViewModel.showSnackbarMessage( + context.getString( + R.string.precise_location_required, + ), + ) - fineLocationState.status.isGranted -> - appViewModel.showSnackbarMessage( - context.getString(R.string.precise_location_required), - ) + viewModel.isLocationEnabled(context) -> + showLocationServicesAlertDialog = true - viewModel.isLocationEnabled(context) -> - showLocationServicesAlertDialog = true - - else -> { - handleAutoTunnelToggle() - } - } - } else { - handleAutoTunnelToggle() - } - }, - ) { - val autoTunnelButtonText = - if (uiState.settings.isAutoTunnelEnabled) { - stringResource(R.string.disable_auto_tunnel) - } else { - stringResource(id = R.string.enable_auto_tunnel) - } - Text(autoTunnelButtonText) - } - } - } - } - Surface( - tonalElevation = 2.dp, - shadowElevation = 2.dp, - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surface, - modifier = Modifier - .fillMaxWidth(fillMaxWidth) - .padding(vertical = 10.dp), - ) { - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Top, - modifier = Modifier.padding(15.dp), - ) { - SectionTitle( - title = stringResource(id = R.string.backend), - padding = screenPadding, - ) - ConfigurationToggle( - stringResource(R.string.use_amnezia), - enabled = - !(uiState.settings.isAutoTunnelEnabled || - uiState.settings.isAlwaysOnVpnEnabled || - (uiState.vpnState.status == TunnelState.UP) || uiState.settings.isKernelEnabled), - checked = uiState.settings.isAmneziaEnabled, - padding = screenPadding, - onCheckChanged = { - viewModel.onToggleAmnezia() - }, - ) - if (kernelSupport) { - ConfigurationToggle( - stringResource(R.string.use_kernel), - enabled = - !(uiState.settings.isAutoTunnelEnabled || - uiState.settings.isAlwaysOnVpnEnabled || - (uiState.vpnState.status == TunnelState.UP)), - checked = uiState.settings.isKernelEnabled, - padding = screenPadding, - onCheckChanged = { - scope.launch { - viewModel.onToggleKernelMode().onFailure { - appViewModel.showSnackbarMessage(it.getMessage(context)) - } - } - }, - ) - } - } - } - Surface( - tonalElevation = 2.dp, - shadowElevation = 2.dp, - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surface, - modifier = - Modifier - .fillMaxWidth(fillMaxWidth) - .padding(vertical = 10.dp) - .padding(bottom = 140.dp), - ) { - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Top, - modifier = Modifier.padding(15.dp), - ) { - SectionTitle( - title = stringResource(id = R.string.other), - padding = screenPadding, - ) - if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) { - ConfigurationToggle( - stringResource(R.string.always_on_vpn_support), - enabled = !uiState.settings.isAutoTunnelEnabled, - checked = uiState.settings.isAlwaysOnVpnEnabled, - padding = screenPadding, - onCheckChanged = { viewModel.onToggleAlwaysOnVPN() }, - ) - ConfigurationToggle( - stringResource(R.string.enabled_app_shortcuts), - enabled = true, - checked = uiState.settings.isShortcutsEnabled, - padding = screenPadding, - onCheckChanged = { viewModel.onToggleShortcutsEnabled() }, - ) - } - ConfigurationToggle( - stringResource(R.string.restart_at_boot), - enabled = true, - checked = uiState.settings.isRestoreOnBootEnabled, - padding = screenPadding, - onCheckChanged = { - viewModel.onToggleRestartAtBoot() - }, - ) - ConfigurationToggle( - stringResource(R.string.enable_app_lock), - enabled = true, - checked = uiState.isPinLockEnabled, - padding = screenPadding, - onCheckChanged = { - if (uiState.isPinLockEnabled) { - viewModel.onPinLockDisabled() - } else { - viewModel.onPinLockEnabled() - navController.navigate(Screen.Lock.route) - } - }, - ) - if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxSize() - .padding(top = 5.dp), - horizontalArrangement = Arrangement.Center, - ) { - TextButton( - enabled = !didExportFiles, - onClick = { showAuthPrompt = true }, - ) { - Text(stringResource(R.string.export_configs)) - } - } - } - } - } - } - } + else -> { + handleAutoTunnelToggle() + } + } + } else { + handleAutoTunnelToggle() + } + }, + ) { + val autoTunnelButtonText = + if (uiState.settings.isAutoTunnelEnabled) { + stringResource(R.string.disable_auto_tunnel) + } else { + stringResource(id = R.string.enable_auto_tunnel) + } + Text(autoTunnelButtonText) + } + } + } + } + Surface( + tonalElevation = 2.dp, + shadowElevation = 2.dp, + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surface, + modifier = + Modifier + .fillMaxWidth(fillMaxWidth) + .padding(vertical = 10.dp), + ) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier.padding(15.dp), + ) { + SectionTitle( + title = stringResource(id = R.string.backend), + padding = screenPadding, + ) + ConfigurationToggle( + stringResource(R.string.use_amnezia), + enabled = + !( + uiState.settings.isAutoTunnelEnabled || + uiState.settings.isAlwaysOnVpnEnabled || + (uiState.vpnState.status == TunnelState.UP) || uiState.settings.isKernelEnabled + ), + checked = uiState.settings.isAmneziaEnabled, + padding = screenPadding, + onCheckChanged = { + viewModel.onToggleAmnezia() + }, + ) + if (kernelSupport) { + ConfigurationToggle( + stringResource(R.string.use_kernel), + enabled = + !( + uiState.settings.isAutoTunnelEnabled || + uiState.settings.isAlwaysOnVpnEnabled || + (uiState.vpnState.status == TunnelState.UP) + ), + checked = uiState.settings.isKernelEnabled, + padding = screenPadding, + onCheckChanged = { + scope.launch { + viewModel.onToggleKernelMode().onFailure { + appViewModel.showSnackbarMessage(it.getMessage(context)) + } + } + }, + ) + } + } + } + Surface( + tonalElevation = 2.dp, + shadowElevation = 2.dp, + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surface, + modifier = + Modifier + .fillMaxWidth(fillMaxWidth) + .padding(vertical = 10.dp) + .padding(bottom = 140.dp), + ) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier.padding(15.dp), + ) { + SectionTitle( + title = stringResource(id = R.string.other), + padding = screenPadding, + ) + if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) { + ConfigurationToggle( + stringResource(R.string.always_on_vpn_support), + enabled = !uiState.settings.isAutoTunnelEnabled, + checked = uiState.settings.isAlwaysOnVpnEnabled, + padding = screenPadding, + onCheckChanged = { viewModel.onToggleAlwaysOnVPN() }, + ) + ConfigurationToggle( + stringResource(R.string.enabled_app_shortcuts), + enabled = true, + checked = uiState.settings.isShortcutsEnabled, + padding = screenPadding, + onCheckChanged = { viewModel.onToggleShortcutsEnabled() }, + ) + } + ConfigurationToggle( + stringResource(R.string.restart_at_boot), + enabled = true, + checked = uiState.settings.isRestoreOnBootEnabled, + padding = screenPadding, + onCheckChanged = { + viewModel.onToggleRestartAtBoot() + }, + ) + ConfigurationToggle( + stringResource(R.string.enable_app_lock), + enabled = true, + checked = uiState.isPinLockEnabled, + padding = screenPadding, + onCheckChanged = { + if (uiState.isPinLockEnabled) { + viewModel.onPinLockDisabled() + } else { + viewModel.onPinLockEnabled() + navController.navigate(Screen.Lock.route) + } + }, + ) + if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .fillMaxSize() + .padding(top = 5.dp), + horizontalArrangement = Arrangement.Center, + ) { + TextButton( + enabled = !didExportFiles, + onClick = { showAuthPrompt = true }, + ) { + Text(stringResource(R.string.export_configs)) + } + } + } + } + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsUiState.kt index f51b98c2..9adcf854 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsUiState.kt @@ -5,10 +5,10 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState data class SettingsUiState( - val settings: Settings = Settings(), - val tunnels: List = emptyList(), - val vpnState: VpnState = VpnState(), - val isLocationDisclosureShown: Boolean = true, - val isBatteryOptimizeDisableShown: Boolean = false, - val isPinLockEnabled: Boolean = false + val settings: Settings = Settings(), + val tunnels: List = emptyList(), + val vpnState: VpnState = VpnState(), + val isLocationDisclosureShown: Boolean = true, + val isBatteryOptimizeDisableShown: Boolean = false, + val isPinLockEnabled: Boolean = false, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt index 704cd914..3b789404 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt @@ -36,222 +36,219 @@ import javax.inject.Provider class SettingsViewModel @Inject constructor( - private val appDataRepository: AppDataRepository, - private val serviceManager: ServiceManager, - private val rootShell: Provider, - private val fileUtils: FileUtils, - @IoDispatcher private val ioDispatcher: CoroutineDispatcher, - vpnService: VpnService + private val appDataRepository: AppDataRepository, + private val serviceManager: ServiceManager, + private val rootShell: Provider, + private val fileUtils: FileUtils, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + vpnService: VpnService, ) : ViewModel() { + private val _kernelSupport = MutableStateFlow(false) + val kernelSupport = _kernelSupport.asStateFlow() - private val _kernelSupport = MutableStateFlow(false) - val kernelSupport = _kernelSupport.asStateFlow() + val uiState = + combine( + appDataRepository.settings.getSettingsFlow(), + appDataRepository.tunnels.getTunnelConfigsFlow(), + vpnService.vpnState, + appDataRepository.appState.generalStateFlow, + ) { settings, tunnels, tunnelState, generalState -> + SettingsUiState( + settings, + tunnels, + tunnelState, + generalState.isLocationDisclosureShown, + generalState.isBatteryOptimizationDisableShown, + generalState.isPinLockEnabled, + ) + } + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), + SettingsUiState(), + ) - val uiState = - combine( - appDataRepository.settings.getSettingsFlow(), - appDataRepository.tunnels.getTunnelConfigsFlow(), - vpnService.vpnState, - appDataRepository.appState.generalStateFlow, - ) { settings, tunnels, tunnelState, generalState -> - SettingsUiState( - settings, - tunnels, - tunnelState, - generalState.isLocationDisclosureShown, - generalState.isBatteryOptimizationDisableShown, - generalState.isPinLockEnabled, - ) - } - .stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), - SettingsUiState(), - ) + fun onSaveTrustedSSID(ssid: String): Result { + val trimmed = ssid.trim() + return if (!uiState.value.settings.trustedNetworkSSIDs.contains(trimmed)) { + uiState.value.settings.trustedNetworkSSIDs.add(trimmed) + saveSettings(uiState.value.settings) + Result.success(Unit) + } else { + Result.failure(WgTunnelExceptions.SsidConflict()) + } + } - fun onSaveTrustedSSID(ssid: String): Result { - val trimmed = ssid.trim() - return if (!uiState.value.settings.trustedNetworkSSIDs.contains(trimmed)) { - uiState.value.settings.trustedNetworkSSIDs.add(trimmed) - saveSettings(uiState.value.settings) - Result.success(Unit) - } else { - Result.failure(WgTunnelExceptions.SsidConflict()) - } - } + fun setLocationDisclosureShown() = viewModelScope.launch { + appDataRepository.appState.setLocationDisclosureShown(true) + } - fun setLocationDisclosureShown() = - viewModelScope.launch { - appDataRepository.appState.setLocationDisclosureShown(true) - } + fun setBatteryOptimizeDisableShown() = viewModelScope.launch { + appDataRepository.appState.setBatteryOptimizationDisableShown(true) + } - fun setBatteryOptimizeDisableShown() = - viewModelScope.launch { - appDataRepository.appState.setBatteryOptimizationDisableShown(true) - } + fun onToggleTunnelOnMobileData() { + saveSettings( + uiState.value.settings.copy( + isTunnelOnMobileDataEnabled = !uiState.value.settings.isTunnelOnMobileDataEnabled, + ), + ) + } - fun onToggleTunnelOnMobileData() { - saveSettings( - uiState.value.settings.copy( - isTunnelOnMobileDataEnabled = !uiState.value.settings.isTunnelOnMobileDataEnabled, - ), - ) - } + fun onDeleteTrustedSSID(ssid: String) { + saveSettings( + uiState.value.settings.copy( + trustedNetworkSSIDs = + (uiState.value.settings.trustedNetworkSSIDs - ssid).toMutableList(), + ), + ) + } - fun onDeleteTrustedSSID(ssid: String) { - saveSettings( - uiState.value.settings.copy( - trustedNetworkSSIDs = - (uiState.value.settings.trustedNetworkSSIDs - ssid).toMutableList(), - ), - ) - } + suspend fun onExportTunnels(files: List): Result { + return fileUtils.saveFilesToZip(files) + } - suspend fun onExportTunnels(files: List): Result { - return fileUtils.saveFilesToZip(files) - } + fun onToggleAutoTunnel(context: Context) = viewModelScope.launch { + val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled + var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused - fun onToggleAutoTunnel(context: Context) = - viewModelScope.launch { - val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled - var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused + if (isAutoTunnelEnabled) { + serviceManager.stopWatcherService(context) + } else { + serviceManager.startWatcherService(context) + isAutoTunnelPaused = false + } + saveSettings( + uiState.value.settings.copy( + isAutoTunnelEnabled = !isAutoTunnelEnabled, + isAutoTunnelPaused = isAutoTunnelPaused, + ), + ) + WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate() + } - if (isAutoTunnelEnabled) { - serviceManager.stopWatcherService(context) - } else { - serviceManager.startWatcherService(context) - isAutoTunnelPaused = false - } - saveSettings( - uiState.value.settings.copy( - isAutoTunnelEnabled = !isAutoTunnelEnabled, - isAutoTunnelPaused = isAutoTunnelPaused, - ), - ) - WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate() - } + fun onToggleAlwaysOnVPN() = viewModelScope.launch { + saveSettings( + uiState.value.settings.copy( + isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled, + ), + ) + } - fun onToggleAlwaysOnVPN() = - viewModelScope.launch { - saveSettings( - uiState.value.settings.copy( - isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled, - ), - ) - } + private fun saveSettings(settings: Settings) = viewModelScope.launch { appDataRepository.settings.save(settings) } - private fun saveSettings(settings: Settings) = - viewModelScope.launch { appDataRepository.settings.save(settings) } + fun onToggleTunnelOnEthernet() { + saveSettings( + uiState.value.settings.copy( + isTunnelOnEthernetEnabled = !uiState.value.settings.isTunnelOnEthernetEnabled, + ), + ) + } - fun onToggleTunnelOnEthernet() { - saveSettings( - uiState.value.settings.copy( - isTunnelOnEthernetEnabled = !uiState.value.settings.isTunnelOnEthernetEnabled, - ), - ) - } + fun isLocationEnabled(context: Context): Boolean { + val locationManager = + context.getSystemService( + Context.LOCATION_SERVICE, + ) as LocationManager + return LocationManagerCompat.isLocationEnabled(locationManager) + } - fun isLocationEnabled(context: Context): Boolean { - val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager - return LocationManagerCompat.isLocationEnabled(locationManager) - } + fun onToggleShortcutsEnabled() { + saveSettings( + uiState.value.settings.copy( + isShortcutsEnabled = !uiState.value.settings.isShortcutsEnabled, + ), + ) + } - fun onToggleShortcutsEnabled() { - saveSettings( - uiState.value.settings.copy( - isShortcutsEnabled = !uiState.value.settings.isShortcutsEnabled, - ), - ) - } + private fun saveKernelMode(on: Boolean) { + saveSettings( + uiState.value.settings.copy( + isKernelEnabled = on, + ), + ) + } - private fun saveKernelMode(on: Boolean) { - saveSettings( - uiState.value.settings.copy( - isKernelEnabled = on, - ), - ) - } + fun onToggleTunnelOnWifi() { + saveSettings( + uiState.value.settings.copy( + isTunnelOnWifiEnabled = !uiState.value.settings.isTunnelOnWifiEnabled, + ), + ) + } - fun onToggleTunnelOnWifi() { - saveSettings( - uiState.value.settings.copy( - isTunnelOnWifiEnabled = !uiState.value.settings.isTunnelOnWifiEnabled, - ), - ) - } + fun onToggleAmnezia() = viewModelScope.launch { + if (uiState.value.settings.isKernelEnabled) { + saveKernelMode(false) + } + saveAmneziaMode(!uiState.value.settings.isAmneziaEnabled) + } - fun onToggleAmnezia() = viewModelScope.launch { - if (uiState.value.settings.isKernelEnabled) { - saveKernelMode(false) - } - saveAmneziaMode(!uiState.value.settings.isAmneziaEnabled) - } + private fun saveAmneziaMode(on: Boolean) { + saveSettings( + uiState.value.settings.copy( + isAmneziaEnabled = on, + ), + ) + } - private fun saveAmneziaMode(on: Boolean) { - saveSettings( - uiState.value.settings.copy( - isAmneziaEnabled = on, - ), - ) - } + suspend fun onToggleKernelMode(): Result { + return withContext(ioDispatcher) { + if (!uiState.value.settings.isKernelEnabled) { + try { + rootShell.get().start() + Timber.i("Root shell accepted!") + saveSettings( + uiState.value.settings.copy( + isKernelEnabled = true, + isAmneziaEnabled = false, + ), + ) + } catch (e: RootShell.RootShellException) { + Timber.e(e) + saveKernelMode(on = false) + return@withContext Result.failure(WgTunnelExceptions.RootDenied()) + } + } else { + saveKernelMode(on = false) + } + Result.success(Unit) + } + } - suspend fun onToggleKernelMode(): Result { - return withContext(ioDispatcher) { - if (!uiState.value.settings.isKernelEnabled) { - try { - rootShell.get().start() - Timber.i("Root shell accepted!") - saveSettings( - uiState.value.settings.copy( - isKernelEnabled = true, - isAmneziaEnabled = false, - ), - ) + fun onToggleRestartOnPing() = viewModelScope.launch { + saveSettings( + uiState.value.settings.copy( + isPingEnabled = !uiState.value.settings.isPingEnabled, + ), + ) + } - } catch (e: RootShell.RootShellException) { - Timber.e(e) - saveKernelMode(on = false) - return@withContext Result.failure(WgTunnelExceptions.RootDenied()) - } - } else { - saveKernelMode(on = false) - } - Result.success(Unit) - } - } + fun checkKernelSupport() = viewModelScope.launch { + val kernelSupport = + withContext(ioDispatcher) { + WgQuickBackend.hasKernelSupport() + } + _kernelSupport.update { + kernelSupport + } + } - fun onToggleRestartOnPing() = viewModelScope.launch { - saveSettings( - uiState.value.settings.copy( - isPingEnabled = !uiState.value.settings.isPingEnabled, - ), - ) - } + fun onPinLockDisabled() = viewModelScope.launch { + PinManager.clearPin() + appDataRepository.appState.setPinLockEnabled(false) + } - fun checkKernelSupport() = viewModelScope.launch { - val kernelSupport = withContext(ioDispatcher) { - WgQuickBackend.hasKernelSupport() - } - _kernelSupport.update { - kernelSupport - } - } + fun onPinLockEnabled() = viewModelScope.launch { + PinManager.initialize(WireGuardAutoTunnel.instance) + appDataRepository.appState.setPinLockEnabled(true) + } - fun onPinLockDisabled() = viewModelScope.launch { - PinManager.clearPin() - appDataRepository.appState.setPinLockEnabled(false) - } - - fun onPinLockEnabled() = viewModelScope.launch { - PinManager.initialize(WireGuardAutoTunnel.instance) - appDataRepository.appState.setPinLockEnabled(true) - } - - fun onToggleRestartAtBoot() = viewModelScope.launch { - saveSettings( - uiState.value.settings.copy( - isRestoreOnBootEnabled = !uiState.value.settings.isRestoreOnBootEnabled, - ), - ) - } + fun onToggleRestartAtBoot() = viewModelScope.launch { + saveSettings( + uiState.value.settings.copy( + isRestoreOnBootEnabled = !uiState.value.settings.isRestoreOnBootEnabled, + ), + ) + } } 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 284fa345..bde52a4c 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 @@ -54,262 +54,268 @@ import com.zaneschepke.wireguardautotunnel.ui.Screen @Composable fun SupportScreen( - viewModel: SupportViewModel = hiltViewModel(), - appViewModel: AppViewModel, - navController: NavController, - focusRequester: FocusRequester + viewModel: SupportViewModel = hiltViewModel(), + appViewModel: AppViewModel, + navController: NavController, + focusRequester: FocusRequester, ) { - val context = LocalContext.current - val fillMaxWidth = .85f + val context = LocalContext.current + val fillMaxWidth = .85f - val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top, - modifier = - Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .focusable(), - ) { - Surface( - tonalElevation = 2.dp, - shadowElevation = 2.dp, - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surface, - modifier = - (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - Modifier - .height(IntrinsicSize.Min) - .fillMaxWidth(fillMaxWidth) - .padding(top = 10.dp) - } else { - Modifier - .fillMaxWidth(fillMaxWidth) - .padding(top = 20.dp) - }) - .padding(bottom = 25.dp), - ) { - Column(modifier = Modifier.padding(20.dp)) { - val forwardIcon = Icons.AutoMirrored.Rounded.ArrowForward - Text( - stringResource(R.string.thank_you), - textAlign = TextAlign.Start, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(bottom = 20.dp), - fontSize = 16.sp, - ) - Text( - stringResource(id = R.string.support_help_text), - textAlign = TextAlign.Start, - fontSize = 16.sp, - modifier = Modifier.padding(bottom = 20.dp), - ) - TextButton( - onClick = { - appViewModel.openWebPage( - context.resources.getString(R.string.docs_url), - context, - ) - }, - modifier = Modifier - .padding(vertical = 5.dp) - .focusRequester(focusRequester), - ) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), - ) { - Row { - val icon = Icons.Rounded.Book - Icon(icon, icon.name) - Text( - stringResource(id = R.string.docs_description), - textAlign = TextAlign.Justify, - modifier = Modifier - .padding(start = 10.dp) - .weight( - weight = 1.0f, - fill = false, - ), - softWrap = true, - ) - } - Icon( - forwardIcon, - forwardIcon.name, - ) - } - } - HorizontalDivider( - thickness = 0.5.dp, - color = MaterialTheme.colorScheme.onBackground, - ) - TextButton( - onClick = { - appViewModel.openWebPage( - context.resources.getString(R.string.telegram_url), - context, - ) - }, - modifier = Modifier.padding(vertical = 5.dp), - ) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), - ) { - Row { - val icon = ImageVector.vectorResource(R.drawable.telegram) - Icon( - icon, - icon.name, - Modifier.size(25.dp), - ) - Text( - stringResource(id = R.string.discord_description), - textAlign = TextAlign.Justify, - modifier = Modifier.padding(start = 10.dp), - ) - } - Icon( - forwardIcon, - forwardIcon.name, - ) - } - } - HorizontalDivider( - thickness = 0.5.dp, - color = MaterialTheme.colorScheme.onBackground, - ) - TextButton( - onClick = { - appViewModel.openWebPage( - context.resources.getString(R.string.github_url), - context, - ) - }, - modifier = Modifier.padding(vertical = 5.dp), - ) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), - ) { - Row { - val icon = ImageVector.vectorResource(R.drawable.github) - Icon( - imageVector = icon, - icon.name, - Modifier.size(25.dp), - ) - Text( - stringResource(id = R.string.open_issue), - textAlign = TextAlign.Justify, - modifier = Modifier.padding(start = 10.dp), - ) - } - Icon( - forwardIcon, - forwardIcon.name, - ) - } - } - HorizontalDivider( - thickness = 0.5.dp, - color = MaterialTheme.colorScheme.onBackground, - ) - TextButton( - onClick = { appViewModel.launchEmail(context) }, - modifier = Modifier.padding(vertical = 5.dp), - ) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), - ) { - Row { - val icon = Icons.Rounded.Mail - Icon(icon, icon.name) - Text( - stringResource(id = R.string.email_description), - textAlign = TextAlign.Justify, - modifier = Modifier.padding(start = 10.dp), - ) - } - Icon( - forwardIcon, - forwardIcon.name, - ) - } - } - if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) { - HorizontalDivider( - thickness = 0.5.dp, - color = MaterialTheme.colorScheme.onBackground, - ) - TextButton( - onClick = { navController.navigate(Screen.Support.Logs.route) }, - modifier = Modifier.padding(vertical = 5.dp), - ) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), - ) { - Row { - val icon = Icons.Rounded.FormatListNumbered - Icon(icon, icon.name) - Text( - stringResource(id = R.string.read_logs), - textAlign = TextAlign.Justify, - modifier = Modifier.padding(start = 10.dp), - ) - } - Icon( - Icons.AutoMirrored.Rounded.ArrowForward, - stringResource(id = R.string.go), - ) - } - } - } - } - } - Spacer(modifier = Modifier.weight(1f)) - Text( - stringResource(id = R.string.privacy_policy), - style = TextStyle(textDecoration = TextDecoration.Underline), - fontSize = 16.sp, - modifier = - Modifier.clickable { - appViewModel.openWebPage( - context.resources.getString(R.string.privacy_policy_url), - context, - ) - }, - ) - Row( - horizontalArrangement = Arrangement.spacedBy(25.dp), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(25.dp), - ) { - val version = buildAnnotatedString { - append(stringResource(id = R.string.version)) - append(": ") - append(BuildConfig.VERSION_NAME) - } - val mode = buildAnnotatedString { - append(stringResource(R.string.mode)) - append(": ") - when (uiState.settings.isKernelEnabled) { - true -> append(stringResource(id = R.string.kernel)) - false -> append(stringResource(id = R.string.userspace)) - } - } - Text(version.text, modifier = Modifier.focusable()) - Text(mode.text) - } - } + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + modifier = + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .focusable(), + ) { + Surface( + tonalElevation = 2.dp, + shadowElevation = 2.dp, + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surface, + modifier = + ( + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + Modifier + .height(IntrinsicSize.Min) + .fillMaxWidth(fillMaxWidth) + .padding(top = 10.dp) + } else { + Modifier + .fillMaxWidth(fillMaxWidth) + .padding(top = 20.dp) + } + ) + .padding(bottom = 25.dp), + ) { + Column(modifier = Modifier.padding(20.dp)) { + val forwardIcon = Icons.AutoMirrored.Rounded.ArrowForward + Text( + stringResource(R.string.thank_you), + textAlign = TextAlign.Start, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 20.dp), + fontSize = 16.sp, + ) + Text( + stringResource(id = R.string.support_help_text), + textAlign = TextAlign.Start, + fontSize = 16.sp, + modifier = Modifier.padding(bottom = 20.dp), + ) + TextButton( + onClick = { + appViewModel.openWebPage( + context.resources.getString(R.string.docs_url), + context, + ) + }, + modifier = + Modifier + .padding(vertical = 5.dp) + .focusRequester(focusRequester), + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Row { + val icon = Icons.Rounded.Book + Icon(icon, icon.name) + Text( + stringResource(id = R.string.docs_description), + textAlign = TextAlign.Justify, + modifier = + Modifier + .padding(start = 10.dp) + .weight( + weight = 1.0f, + fill = false, + ), + softWrap = true, + ) + } + Icon( + forwardIcon, + forwardIcon.name, + ) + } + } + HorizontalDivider( + thickness = 0.5.dp, + color = MaterialTheme.colorScheme.onBackground, + ) + TextButton( + onClick = { + appViewModel.openWebPage( + context.resources.getString(R.string.telegram_url), + context, + ) + }, + modifier = Modifier.padding(vertical = 5.dp), + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Row { + val icon = ImageVector.vectorResource(R.drawable.telegram) + Icon( + icon, + icon.name, + Modifier.size(25.dp), + ) + Text( + stringResource(id = R.string.discord_description), + textAlign = TextAlign.Justify, + modifier = Modifier.padding(start = 10.dp), + ) + } + Icon( + forwardIcon, + forwardIcon.name, + ) + } + } + HorizontalDivider( + thickness = 0.5.dp, + color = MaterialTheme.colorScheme.onBackground, + ) + TextButton( + onClick = { + appViewModel.openWebPage( + context.resources.getString(R.string.github_url), + context, + ) + }, + modifier = Modifier.padding(vertical = 5.dp), + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Row { + val icon = ImageVector.vectorResource(R.drawable.github) + Icon( + imageVector = icon, + icon.name, + Modifier.size(25.dp), + ) + Text( + stringResource(id = R.string.open_issue), + textAlign = TextAlign.Justify, + modifier = Modifier.padding(start = 10.dp), + ) + } + Icon( + forwardIcon, + forwardIcon.name, + ) + } + } + HorizontalDivider( + thickness = 0.5.dp, + color = MaterialTheme.colorScheme.onBackground, + ) + TextButton( + onClick = { appViewModel.launchEmail(context) }, + modifier = Modifier.padding(vertical = 5.dp), + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Row { + val icon = Icons.Rounded.Mail + Icon(icon, icon.name) + Text( + stringResource(id = R.string.email_description), + textAlign = TextAlign.Justify, + modifier = Modifier.padding(start = 10.dp), + ) + } + Icon( + forwardIcon, + forwardIcon.name, + ) + } + } + if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) { + HorizontalDivider( + thickness = 0.5.dp, + color = MaterialTheme.colorScheme.onBackground, + ) + TextButton( + onClick = { navController.navigate(Screen.Support.Logs.route) }, + modifier = Modifier.padding(vertical = 5.dp), + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Row { + val icon = Icons.Rounded.FormatListNumbered + Icon(icon, icon.name) + Text( + stringResource(id = R.string.read_logs), + textAlign = TextAlign.Justify, + modifier = Modifier.padding(start = 10.dp), + ) + } + Icon( + Icons.AutoMirrored.Rounded.ArrowForward, + stringResource(id = R.string.go), + ) + } + } + } + } + } + Spacer(modifier = Modifier.weight(1f)) + Text( + stringResource(id = R.string.privacy_policy), + style = TextStyle(textDecoration = TextDecoration.Underline), + fontSize = 16.sp, + modifier = + Modifier.clickable { + appViewModel.openWebPage( + context.resources.getString(R.string.privacy_policy_url), + context, + ) + }, + ) + Row( + horizontalArrangement = Arrangement.spacedBy(25.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(25.dp), + ) { + val version = + buildAnnotatedString { + append(stringResource(id = R.string.version)) + append(": ") + append(BuildConfig.VERSION_NAME) + } + val mode = + buildAnnotatedString { + append(stringResource(R.string.mode)) + append(": ") + when (uiState.settings.isKernelEnabled) { + true -> append(stringResource(id = R.string.kernel)) + false -> append(stringResource(id = R.string.userspace)) + } + } + Text(version.text, modifier = Modifier.focusable()) + Text(mode.text) + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportViewModel.kt index 7b14d9a8..6874a3b7 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportViewModel.kt @@ -11,16 +11,17 @@ import kotlinx.coroutines.flow.stateIn import javax.inject.Inject @HiltViewModel -class SupportViewModel @Inject constructor(settingsRepository: SettingsRepository) : - ViewModel() { - - val uiState = - settingsRepository - .getSettingsFlow() - .map { SupportUiState(it) } - .stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), - SupportUiState(), - ) +class SupportViewModel +@Inject +constructor(settingsRepository: SettingsRepository) : + ViewModel() { + val uiState = + settingsRepository + .getSettingsFlow() + .map { SupportUiState(it) } + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), + SupportUiState(), + ) } 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 7ad015bf..dd1393df 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 @@ -42,85 +42,88 @@ import kotlinx.coroutines.launch @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable fun LogsScreen(viewModel: LogsViewModel = hiltViewModel()) { + val logs = viewModel.logs - val logs = viewModel.logs + val context = LocalContext.current - val context = LocalContext.current + val lazyColumnListState = rememberLazyListState() + val clipboardManager: ClipboardManager = LocalClipboardManager.current + val scope = rememberCoroutineScope() - val lazyColumnListState = rememberLazyListState() - val clipboardManager: ClipboardManager = LocalClipboardManager.current - val scope = rememberCoroutineScope() + LaunchedEffect(logs.size) { + scope.launch { + lazyColumnListState.animateScrollToItem(logs.size) + } + } - LaunchedEffect(logs.size) { - scope.launch { - lazyColumnListState.animateScrollToItem(logs.size) - } - } - - Scaffold( - floatingActionButton = { - FloatingActionButton( - onClick = { - scope.launch { - viewModel.saveLogsToFile().onSuccess { - Toast.makeText( - context, - context.getString(R.string.logs_saved), - Toast.LENGTH_SHORT, - ).show() - } - } - }, - shape = RoundedCornerShape(16.dp), - containerColor = MaterialTheme.colorScheme.primary, - ) { - val icon = Icons.Filled.Save - Icon( - imageVector = icon, - contentDescription = icon.name, - tint = MaterialTheme.colorScheme.onPrimary, - ) - } - }, - ) { - LazyColumn( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top), - state = lazyColumnListState, - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 24.dp), - ) { - itemsIndexed( - logs, - key = { index, _ -> index }, - contentType = { _: Int, _: LogMessage -> null }, - ) { _, it -> - Row( - horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start), - verticalAlignment = Alignment.Top, - modifier = Modifier - .fillMaxSize() - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - onClick = { - clipboardManager.setText(annotatedString = AnnotatedString(it.toString())) - }, - ), - ) { - val fontSize = 10.sp - Text(text = it.tag, modifier = Modifier.fillMaxSize(0.3f), fontSize = fontSize) - LogTypeLabel(color = Color(it.level.color())) { - Text( - text = it.level.signifier, - textAlign = TextAlign.Center, - fontSize = fontSize, - ) - } - Text("${it.message} - ${it.time}", fontSize = fontSize) - } - } - } - } + Scaffold( + floatingActionButton = { + FloatingActionButton( + onClick = { + scope.launch { + viewModel.saveLogsToFile().onSuccess { + Toast.makeText( + context, + context.getString(R.string.logs_saved), + Toast.LENGTH_SHORT, + ).show() + } + } + }, + shape = RoundedCornerShape(16.dp), + containerColor = MaterialTheme.colorScheme.primary, + ) { + val icon = Icons.Filled.Save + Icon( + imageVector = icon, + contentDescription = icon.name, + tint = MaterialTheme.colorScheme.onPrimary, + ) + } + }, + ) { + LazyColumn( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top), + state = lazyColumnListState, + modifier = + Modifier + .fillMaxSize() + .padding(horizontal = 24.dp), + ) { + itemsIndexed( + logs, + key = { index, _ -> index }, + contentType = { _: Int, _: LogMessage -> null }, + ) { _, it -> + Row( + horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start), + verticalAlignment = Alignment.Top, + modifier = + Modifier + .fillMaxSize() + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { + clipboardManager.setText( + annotatedString = AnnotatedString(it.toString()), + ) + }, + ), + ) { + val fontSize = 10.sp + Text(text = it.tag, modifier = Modifier.fillMaxSize(0.3f), fontSize = fontSize) + LogTypeLabel(color = Color(it.level.color())) { + Text( + text = it.level.signifier, + textAlign = TextAlign.Center, + fontSize = fontSize, + ) + } + Text("${it.message} - ${it.time}", fontSize = fontSize) + } + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsViewModel.kt index ae86bb83..782970f5 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsViewModel.kt @@ -20,36 +20,37 @@ import javax.inject.Inject @HiltViewModel class LogsViewModel -@Inject constructor( - private val localLogCollector: LocalLogCollector, - private val fileUtils: FileUtils, - @IoDispatcher private val ioDispatcher: CoroutineDispatcher, - @MainDispatcher private val mainDispatcher: CoroutineDispatcher +@Inject +constructor( + private val localLogCollector: LocalLogCollector, + private val fileUtils: FileUtils, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + @MainDispatcher private val mainDispatcher: CoroutineDispatcher, ) : ViewModel() { + val logs = mutableStateListOf() - val logs = mutableStateListOf() + init { + viewModelScope.launch(ioDispatcher) { + localLogCollector.bufferedLogs.chunked(500, Duration.ofSeconds(1)).collect { + withContext(mainDispatcher) { + logs.addAll(it) + } + if (logs.size > Constants.LOG_BUFFER_SIZE) { + withContext(mainDispatcher) { + logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt()) + } + } + } + } + } - init { - viewModelScope.launch(ioDispatcher) { - localLogCollector.bufferedLogs.chunked(500, Duration.ofSeconds(1)).collect { - withContext(mainDispatcher) { - logs.addAll(it) - } - if (logs.size > Constants.LOG_BUFFER_SIZE) { - withContext(mainDispatcher) { - logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt()) - } - } - } - } - } - - suspend fun saveLogsToFile(): Result { - val file = localLogCollector.getLogFile().getOrElse { - return Result.failure(it) - } - val fileContent = fileUtils.readBytesFromFile(file) - val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt" - return fileUtils.saveByteArrayToDownloads(fileContent, fileName) - } + suspend fun saveLogsToFile(): Result { + val file = + localLogCollector.getLogFile().getOrElse { + return Result.failure(it) + } + val fileContent = fileUtils.readBytesFromFile(file) + val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt" + return fileUtils.saveByteArrayToDownloads(fileContent, fileName) + } } 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 51f23a8a..a17585f1 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 @@ -16,20 +16,20 @@ import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowCompat private val DarkColorScheme = - darkColorScheme( - // primary = Purple80, - primary = virdigris, - secondary = virdigris, - // secondary = PurpleGrey80, - tertiary = virdigris, - // tertiary = Pink80 - ) + darkColorScheme( + // primary = Purple80, + primary = virdigris, + secondary = virdigris, + // secondary = PurpleGrey80, + tertiary = virdigris, + // tertiary = Pink80 + ) private val LightColorScheme = - lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40, + lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40, /* Other default colors to override background = Color(0xFFFFFBFE), surface = Color(0xFFFFFBFE), @@ -39,43 +39,43 @@ private val LightColorScheme = onBackground = Color(0xFF1C1B1F), onSurface = Color(0xFF1C1B1F), */ - ) + ) @Composable fun WireguardAutoTunnelTheme( - // force dark theme - darkTheme: Boolean = true, - // darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - // turning off dynamic color for now - dynamicColor: Boolean = false, - content: @Composable () -> Unit + // force dark theme + darkTheme: Boolean = true, + // darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + // turning off dynamic color for now + dynamicColor: Boolean = false, + content: @Composable () -> Unit, ) { - val colorScheme = - when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } + val colorScheme = + when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } - darkTheme -> DarkColorScheme - else -> LightColorScheme - } - val view = LocalView.current - if (!view.isInEditMode) { - SideEffect { - val window = (view.context as Activity).window - WindowCompat.setDecorFitsSystemWindows(window, false) - window.statusBarColor = Color.Transparent.toArgb() - window.navigationBarColor = Color.Transparent.toArgb() - WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = - !darkTheme - } - } + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + WindowCompat.setDecorFitsSystemWindows(window, false) + window.statusBarColor = Color.Transparent.toArgb() + window.navigationBarColor = Color.Transparent.toArgb() + WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = + !darkTheme + } + } - 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 6fad95b5..0a1a9c93 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 @@ -8,15 +8,15 @@ import androidx.compose.ui.unit.sp // Set of Material typography styles to start with val Typography = - Typography( - bodyLarge = - TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp, - ), + Typography( + bodyLarge = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp, + ), /* Other default text styles to override titleLarge = TextStyle( fontFamily = FontFamily.Default, @@ -33,4 +33,4 @@ val Typography = letterSpacing = 0.5.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 9ff90dc1..afa7c884 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt @@ -1,44 +1,42 @@ package com.zaneschepke.wireguardautotunnel.util object Constants { + const val BASE_LOG_FILE_NAME = "wg_tunnel_logs" + const val LOG_BUFFER_SIZE = 3_000L - const val BASE_LOG_FILE_NAME = "wg_tunnel_logs" - const val LOG_BUFFER_SIZE = 3_000L + const val MANUAL_TUNNEL_CONFIG_ID = "0" + const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1_000L // 10 minutes + const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L + const val VPN_CONNECTED_NOTIFICATION_DELAY = 3_000L + const val TOGGLE_TUNNEL_DELAY = 300L + const val WATCHER_COLLECTION_DELAY = 1_000L + const val CONF_FILE_EXTENSION = ".conf" + const val ZIP_FILE_EXTENSION = ".zip" + const val URI_CONTENT_SCHEME = "content" + const val ALLOWED_FILE_TYPES = "*/*" + const val TEXT_MIME_TYPE = "text/plain" + const val ZIP_FILE_MIME_TYPE = "application/zip" + const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs" + const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs" + const val ALWAYS_ON_VPN_ACTION = "android.net.VpnService" + const val EMAIL_MIME_TYPE = "plain/text" + const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024 - const val MANUAL_TUNNEL_CONFIG_ID = "0" - const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1_000L // 10 minutes - const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L - const val VPN_CONNECTED_NOTIFICATION_DELAY = 3_000L - const val TOGGLE_TUNNEL_DELAY = 300L - const val WATCHER_COLLECTION_DELAY = 1_000L - const val CONF_FILE_EXTENSION = ".conf" - const val ZIP_FILE_EXTENSION = ".zip" - const val URI_CONTENT_SCHEME = "content" - const val ALLOWED_FILE_TYPES = "*/*" - const val TEXT_MIME_TYPE = "text/plain" - const val ZIP_FILE_MIME_TYPE = "application/zip" - const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs" - const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs" - const val ALWAYS_ON_VPN_ACTION = "android.net.VpnService" - const val EMAIL_MIME_TYPE = "plain/text" - const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024 + const val SUBSCRIPTION_TIMEOUT = 5_000L + const val FOCUS_REQUEST_DELAY = 500L - const val SUBSCRIPTION_TIMEOUT = 5_000L - const val FOCUS_REQUEST_DELAY = 500L + const val DEFAULT_PING_IP = "1.1.1.1" + const val PING_TIMEOUT = 5_000L + const val VPN_RESTART_DELAY = 1_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 VPN_RESTART_DELAY = 1_000L - const val PING_INTERVAL = 60_000L - const val PING_COOLDOWN = PING_INTERVAL * 60 //one hour + const val ALLOWED_DISPLAY_NAME_LENGTH = 20 - const val ALLOWED_DISPLAY_NAME_LENGTH = 20 + const val TUNNEL_EXTRA_KEY = "tunnelId" - const val TUNNEL_EXTRA_KEY = "tunnelId" - - const val UNREADABLE_SSID = "" - - val amneziaProperties = listOf("Jc", "Jmin", "Jmax", "S1", "S2", "H1", "H2", "H3", "H4") - const val QR_CODE_NAME_PROPERTY = "# Name =" + const val UNREADABLE_SSID = "" + val amneziaProperties = listOf("Jc", "Jmin", "Jmax", "S1", "S2", "H1", "H2", "H3", "H4") + const val QR_CODE_NAME_PROPERTY = "# Name =" } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt index f53e5acd..cc4a2993 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt @@ -27,8 +27,8 @@ import java.util.concurrent.ConcurrentLinkedQueue import kotlin.coroutines.cancellation.CancellationException 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 } @@ -40,47 +40,47 @@ typealias TunnelConfigs = List typealias Packages = List 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 Config.toWgQuickString(): String { - val amQuick = toAwgQuickString() - val lines = amQuick.lines().toMutableList() - val linesIterator = lines.iterator() - while (linesIterator.hasNext()) { - val next = linesIterator.next() - Constants.amneziaProperties.forEach { - if (next.startsWith(it, ignoreCase = true)) { - linesIterator.remove() - } - } - } - return lines.joinToString(System.lineSeparator()) + val amQuick = toAwgQuickString() + val lines = amQuick.lines().toMutableList() + val linesIterator = lines.iterator() + while (linesIterator.hasNext()) { + val next = linesIterator.next() + Constants.amneziaProperties.forEach { + if (next.startsWith(it, ignoreCase = true)) { + linesIterator.remove() + } + } + } + return lines.joinToString(System.lineSeparator()) } fun Throwable.getMessage(context: Context): String { - return when (this) { - is WgTunnelExceptions -> this.getMessage(context) - else -> this.message ?: StringValue.StringResource(R.string.unknown_error).asString(context) - } + return when (this) { + is WgTunnelExceptions -> this.getMessage(context) + else -> this.message ?: StringValue.StringResource(R.string.unknown_error).asString(context) + } } /** @@ -89,60 +89,56 @@ fun Throwable.getMessage(context: Context): String { * 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()) + } + } + } +} @OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class) fun Flow.chunked(size: Int, time: Duration) = channelFlow { - coroutineScope { - val channel = asChannel(this@chunked).chunked(this, size, time) - try { - while (!channel.isClosedForReceive) { - send(channel.receive()) - } - } catch (e: ClosedReceiveChannelException) { - // Channel was closed by the flow completing, nothing to do - Timber.w(e) - } catch (e: CancellationException) { - channel.cancel(e) - throw e - } catch (e: Exception) { - channel.cancel(CancellationException("Closing channel due to flow exception", e)) - throw e - } - } + coroutineScope { + val channel = asChannel(this@chunked).chunked(this, size, time) + try { + while (!channel.isClosedForReceive) { + send(channel.receive()) + } + } catch (e: ClosedReceiveChannelException) { + // Channel was closed by the flow completing, nothing to do + Timber.w(e) + } catch (e: CancellationException) { + channel.cancel(e) + throw e + } catch (e: Exception) { + channel.cancel(CancellationException("Closing channel due to flow exception", e)) + throw e + } + } } @ExperimentalCoroutinesApi fun CoroutineScope.asChannel(flow: Flow): ReceiveChannel = produce { - flow.collect { value -> - channel.send(value) - } + flow.collect { value -> + channel.send(value) + } } - - - 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 574741d8..e9f4a399 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/FileUtils.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/FileUtils.kt @@ -18,113 +18,111 @@ import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream class FileUtils( - private val context: Context, - private val ioDispatcher: CoroutineDispatcher, + private val context: Context, + private val ioDispatcher: CoroutineDispatcher, ) { + suspend fun readBytesFromFile(file: File): ByteArray { + return withContext(ioDispatcher) { + FileInputStream(file).use { + it.readBytes() + } + } + } - suspend fun readBytesFromFile(file: File): ByteArray { - return withContext(ioDispatcher) { - FileInputStream(file).use { - it.readBytes() - } - } - } + suspend fun readTextFromFileName(fileName: String): String { + return withContext(ioDispatcher) { + context.assets.open(fileName).use { stream -> + stream.bufferedReader(Charsets.UTF_8).use { + it.readText() + } + } + } + } - suspend fun readTextFromFileName(fileName: String): String { - return withContext(ioDispatcher) { - context.assets.open(fileName).use { stream -> - stream.bufferedReader(Charsets.UTF_8).use { - it.readText() - } - } - } - } + suspend fun saveByteArrayToDownloads(content: ByteArray, fileName: String): Result { + return withContext(ioDispatcher) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val contentValues = + ContentValues().apply { + put(MediaColumns.DISPLAY_NAME, fileName) + put(MediaColumns.MIME_TYPE, Constants.TEXT_MIME_TYPE) + put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) + } + val resolver = context.contentResolver + val uri = + resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) + if (uri != null) { + resolver.openOutputStream(uri).use { output -> + output?.write(content) + } + } + } else { + val target = + File( + Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS, + ), + fileName, + ) + FileOutputStream(target).use { output -> + output.write(content) + } + } + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } + } + } - suspend fun saveByteArrayToDownloads(content: ByteArray, fileName: String): Result { - return withContext(ioDispatcher) { - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - val contentValues = - ContentValues().apply { - put(MediaColumns.DISPLAY_NAME, fileName) - put(MediaColumns.MIME_TYPE, Constants.TEXT_MIME_TYPE) - put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) - } - val resolver = context.contentResolver - val uri = - resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) - if (uri != null) { - resolver.openOutputStream(uri).use { output -> - output?.write(content) - } - } - } else { - val target = - File( - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), - fileName, - ) - FileOutputStream(target).use { output -> - output.write(content) - } - } - Result.success(Unit) - } catch (e: Exception) { - Result.failure(e) - } - } - } + suspend fun saveFilesToZip(files: List): Result { + return withContext(ioDispatcher) { + try { + val zipOutputStream = + createDownloadsFileOutputStream( + "wg-export_${Instant.now().epochSecond}.zip", + Constants.ZIP_FILE_MIME_TYPE, + ) + ZipOutputStream(zipOutputStream).use { zos -> + files.forEach { file -> + val entry = ZipEntry(file.name) + zos.putNextEntry(entry) + if (file.isFile) { + file.inputStream().use { fis -> fis.copyTo(zos) } + } + } + return@withContext Result.success(Unit) + } + } catch (e: Exception) { + Timber.e(e) + Result.failure(WgTunnelExceptions.ConfigExportFailed()) + } + } + } - suspend fun saveFilesToZip(files: List): Result { - return withContext(ioDispatcher) { - try { - val zipOutputStream = - createDownloadsFileOutputStream( - "wg-export_${Instant.now().epochSecond}.zip", - Constants.ZIP_FILE_MIME_TYPE, - ) - ZipOutputStream(zipOutputStream).use { zos -> - files.forEach { file -> - val entry = ZipEntry(file.name) - zos.putNextEntry(entry) - if (file.isFile) { - file.inputStream().use { fis -> fis.copyTo(zos) } - } - } - return@withContext Result.success(Unit) - } - } catch (e: Exception) { - Timber.e(e) - Result.failure(WgTunnelExceptions.ConfigExportFailed()) - } - } - } - - //TODO issue with android 9 - private fun createDownloadsFileOutputStream( - fileName: String, - mimeType: String = Constants.ALLOWED_FILE_TYPES - ): OutputStream? { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - val resolver = context.contentResolver - val contentValues = - ContentValues().apply { - put(MediaColumns.DISPLAY_NAME, fileName) - put(MediaColumns.MIME_TYPE, mimeType) - put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) - } - val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) - if (uri != null) { - return resolver.openOutputStream(uri) - } - } else { - val target = - File( - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), - fileName, - ) - return target.outputStream() - } - return null - } + // TODO issue with android 9 + private fun createDownloadsFileOutputStream(fileName: String, mimeType: String = Constants.ALLOWED_FILE_TYPES): OutputStream? { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val resolver = context.contentResolver + val contentValues = + ContentValues().apply { + put(MediaColumns.DISPLAY_NAME, fileName) + put(MediaColumns.MIME_TYPE, mimeType) + put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) + } + val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) + if (uri != null) { + return resolver.openOutputStream(uri) + } + } else { + val target = + File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), + fileName, + ) + return target.outputStream() + } + return null + } } 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 0b3eaa6e..248a7f80 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 2cd1fd6d..03b06136 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 353aedf8..cdca8ebb 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/StringValue.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/StringValue.kt @@ -4,21 +4,20 @@ 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/WgTunnelExceptions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/WgTunnelExceptions.kt index 9019398e..afd38f40 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/WgTunnelExceptions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/WgTunnelExceptions.kt @@ -4,127 +4,156 @@ import android.content.Context import com.zaneschepke.wireguardautotunnel.R sealed class WgTunnelExceptions : Exception() { - abstract fun getMessage(context: Context): String - data class General(private val userMessage: StringValue) : WgTunnelExceptions() { - override fun getMessage(context: Context): String { - return userMessage.asString(context) - } - } + abstract fun getMessage(context: Context): String - data class SsidConflict(private val userMessage: StringValue = StringValue.StringResource(R.string.error_ssid_exists)) : - WgTunnelExceptions() { - override fun getMessage(context: Context): String { - return userMessage.asString(context) - } - } + data class General(private val userMessage: StringValue) : WgTunnelExceptions() { + override fun getMessage(context: Context): String { + return userMessage.asString(context) + } + } - data class ConfigExportFailed( - private val userMessage: StringValue = StringValue.StringResource( - R.string.export_configs_failed, - ) - ) : WgTunnelExceptions() { - override fun getMessage(context: Context): String { - return userMessage.asString(context) - } - } + data class SsidConflict( + private val userMessage: StringValue = + StringValue.StringResource( + R.string.error_ssid_exists, + ), + ) : + WgTunnelExceptions() { + override fun getMessage(context: Context): String { + return userMessage.asString(context) + } + } - data class ConfigParseError(private val appendMessage: StringValue = StringValue.Empty) : - WgTunnelExceptions() { - override fun getMessage(context: Context): String { - return StringValue.StringResource(R.string.config_parse_error).asString(context) + ( - if (appendMessage != StringValue.Empty) ": ${appendMessage.asString(context)}" else "") - } - } + data class ConfigExportFailed( + private val userMessage: StringValue = + StringValue.StringResource( + R.string.export_configs_failed, + ), + ) : WgTunnelExceptions() { + override fun getMessage(context: Context): String { + return userMessage.asString(context) + } + } - data class RootDenied(private val userMessage: StringValue = StringValue.StringResource(R.string.error_root_denied)) : - WgTunnelExceptions() { - override fun getMessage(context: Context): String { - return userMessage.asString(context) - } - } + data class ConfigParseError(private val appendMessage: StringValue = StringValue.Empty) : + WgTunnelExceptions() { + override fun getMessage(context: Context): String { + return StringValue.StringResource(R.string.config_parse_error).asString(context) + ( + if (appendMessage != StringValue.Empty) ": ${appendMessage.asString(context)}" else "" + ) + } + } - data class InvalidQrCode(private val userMessage: StringValue = StringValue.StringResource(R.string.error_invalid_code)) : - WgTunnelExceptions() { - override fun getMessage(context: Context): String { - return userMessage.asString(context) - } - } + data class RootDenied( + private val userMessage: StringValue = + StringValue.StringResource( + R.string.error_root_denied, + ), + ) : + WgTunnelExceptions() { + override fun getMessage(context: Context): String { + return userMessage.asString(context) + } + } - data class InvalidFileExtension( - private val userMessage: StringValue = StringValue.StringResource( - R.string.error_file_extension, - ) - ) : WgTunnelExceptions() { - override fun getMessage(context: Context): String { - return userMessage.asString(context) - } - } + data class InvalidQrCode( + private val userMessage: StringValue = + StringValue.StringResource( + R.string.error_invalid_code, + ), + ) : + WgTunnelExceptions() { + override fun getMessage(context: Context): String { + return userMessage.asString(context) + } + } - data class FileReadFailed(private val userMessage: StringValue = StringValue.StringResource(R.string.error_file_format)) : - WgTunnelExceptions() { - override fun getMessage(context: Context): String { - return userMessage.asString(context) - } - } + data class InvalidFileExtension( + private val userMessage: StringValue = + StringValue.StringResource( + R.string.error_file_extension, + ), + ) : WgTunnelExceptions() { + override fun getMessage(context: Context): String { + return userMessage.asString(context) + } + } - data class AuthenticationFailed( - private val userMessage: StringValue = StringValue.StringResource( - R.string.error_authentication_failed, - ) - ) : WgTunnelExceptions() { - override fun getMessage(context: Context): String { - return userMessage.asString(context) - } - } + data class FileReadFailed( + private val userMessage: StringValue = + StringValue.StringResource( + R.string.error_file_format, + ), + ) : + WgTunnelExceptions() { + override fun getMessage(context: Context): String { + return userMessage.asString(context) + } + } - data class AuthorizationFailed( - private val userMessage: StringValue = StringValue.StringResource( - R.string.error_authorization_failed, - ) - ) : WgTunnelExceptions() { - override fun getMessage(context: Context): String { - return userMessage.asString(context) - } - } + data class AuthenticationFailed( + private val userMessage: StringValue = + StringValue.StringResource( + R.string.error_authentication_failed, + ), + ) : WgTunnelExceptions() { + override fun getMessage(context: Context): String { + return userMessage.asString(context) + } + } - data class BackgroundLocationRequired( - private val userMessage: StringValue = StringValue.StringResource( - R.string.background_location_required, - ) - ) : WgTunnelExceptions() { - override fun getMessage(context: Context): String { - return userMessage.asString(context) - } - } + data class AuthorizationFailed( + private val userMessage: StringValue = + StringValue.StringResource( + R.string.error_authorization_failed, + ), + ) : WgTunnelExceptions() { + override fun getMessage(context: Context): String { + return userMessage.asString(context) + } + } - data class LocationServicesRequired( - private val userMessage: StringValue = StringValue.StringResource( - R.string.location_services_required, - ) - ) : WgTunnelExceptions() { - override fun getMessage(context: Context): String { - return userMessage.asString(context) - } - } + data class BackgroundLocationRequired( + private val userMessage: StringValue = + StringValue.StringResource( + R.string.background_location_required, + ), + ) : WgTunnelExceptions() { + override fun getMessage(context: Context): String { + return userMessage.asString(context) + } + } - data class PreciseLocationRequired( - private val userMessage: StringValue = StringValue.StringResource( - R.string.precise_location_required, - ) - ) : WgTunnelExceptions() { - override fun getMessage(context: Context): String { - return userMessage.asString(context) - } - } + data class LocationServicesRequired( + private val userMessage: StringValue = + StringValue.StringResource( + R.string.location_services_required, + ), + ) : WgTunnelExceptions() { + override fun getMessage(context: Context): String { + return userMessage.asString(context) + } + } - data class FileExplorerRequired( - private val userMessage: StringValue = StringValue.StringResource( - R.string.error_no_file_explorer, - ) - ) : WgTunnelExceptions() { - override fun getMessage(context: Context): String { - return userMessage.asString(context) - } - } + data class PreciseLocationRequired( + private val userMessage: StringValue = + StringValue.StringResource( + R.string.precise_location_required, + ), + ) : WgTunnelExceptions() { + override fun getMessage(context: Context): String { + return userMessage.asString(context) + } + } + + data class FileExplorerRequired( + private val userMessage: StringValue = + StringValue.StringResource( + R.string.error_no_file_explorer, + ), + ) : WgTunnelExceptions() { + override fun getMessage(context: Context): String { + return userMessage.asString(context) + } + } } - diff --git a/app/src/test/java/com/zaneschepke/wireguardautotunnel/ExampleUnitTest.kt b/app/src/test/java/com/zaneschepke/wireguardautotunnel/ExampleUnitTest.kt index 435f6ba2..51bba8bc 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 03272a61..a84cf0cc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,9 +1,25 @@ plugins { - alias(libs.plugins.android.application) apply false - alias(libs.plugins.kotlin.android) apply false - alias(libs.plugins.hilt.android) apply false - alias(libs.plugins.kotlinxSerialization) apply false - alias(libs.plugins.ksp) apply false - alias(libs.plugins.androidLibrary) apply false - alias(libs.plugins.compose.compiler) apply false + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.hilt.android) apply false + alias(libs.plugins.kotlinxSerialization) apply false + alias(libs.plugins.ksp) apply false + alias(libs.plugins.androidLibrary) apply false + alias(libs.plugins.compose.compiler) apply false + alias(libs.plugins.ktlint) +} + +subprojects { + apply { + plugin(rootProject.libs.plugins.ktlint.get().pluginId) + } + + ktlint { + debug.set(false) + verbose.set(true) + android.set(true) + outputToConsole.set(true) + ignoreFailures.set(false) + enableExperimentalRules.set(true) + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 761c67a6..1e4ebced 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,6 +33,7 @@ gradlePlugins-grgit = "5.2.2" #plugins material = "1.12.0" +gradlePlugins-ktlint="12.1.1" [libraries] @@ -101,4 +102,5 @@ ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } androidLibrary = { id = "com.android.library", version.ref = "androidGradlePlugin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "gradlePlugins-ktlint" } grgit = { id = "org.ajoberstar.grgit.service", version.ref = "gradlePlugins-grgit" } \ No newline at end of file diff --git a/logcatter/build.gradle.kts b/logcatter/build.gradle.kts index 0252ed2b..285ec918 100644 --- a/logcatter/build.gradle.kts +++ b/logcatter/build.gradle.kts @@ -1,50 +1,50 @@ plugins { - alias(libs.plugins.androidLibrary) - alias(libs.plugins.kotlin.android) + alias(libs.plugins.androidLibrary) + alias(libs.plugins.kotlin.android) } android { - namespace = "com.zaneschepke.logcatter" - compileSdk = Constants.TARGET_SDK + namespace = "com.zaneschepke.logcatter" + compileSdk = Constants.TARGET_SDK - defaultConfig { - minSdk = Constants.MIN_SDK + defaultConfig { + minSdk = Constants.MIN_SDK - 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", - ) - } + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } - 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) - // logging - implementation(libs.timber) + // logging + implementation(libs.timber) } diff --git a/logcatter/src/androidTest/java/com/zaneschepke/ExampleInstrumentedTest.kt b/logcatter/src/androidTest/java/com/zaneschepke/ExampleInstrumentedTest.kt index 5514a875..e427f4c2 100644 --- a/logcatter/src/androidTest/java/com/zaneschepke/ExampleInstrumentedTest.kt +++ b/logcatter/src/androidTest/java/com/zaneschepke/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.test", appContext.packageName) - } + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.zaneschepke.test", appContext.packageName) + } } diff --git a/logcatter/src/main/java/com/zaneschepke/logcatter/LocalLogCollector.kt b/logcatter/src/main/java/com/zaneschepke/logcatter/LocalLogCollector.kt index f0de2007..a92c53da 100644 --- a/logcatter/src/main/java/com/zaneschepke/logcatter/LocalLogCollector.kt +++ b/logcatter/src/main/java/com/zaneschepke/logcatter/LocalLogCollector.kt @@ -5,9 +5,11 @@ import kotlinx.coroutines.flow.Flow import java.io.File interface LocalLogCollector { - fun start(onLogMessage: ((message: LogMessage) -> Unit)? = null) - fun stop() - suspend fun getLogFile(): Result - val bufferedLogs: Flow -} + fun start(onLogMessage: ((message: LogMessage) -> Unit)? = null) + fun stop() + + suspend fun getLogFile(): Result + + val bufferedLogs: Flow +} diff --git a/logcatter/src/main/java/com/zaneschepke/logcatter/LogcatUtil.kt b/logcatter/src/main/java/com/zaneschepke/logcatter/LogcatUtil.kt new file mode 100644 index 00000000..c15f2ad6 --- /dev/null +++ b/logcatter/src/main/java/com/zaneschepke/logcatter/LogcatUtil.kt @@ -0,0 +1,276 @@ +package com.zaneschepke.logcatter + +import android.content.Context +import com.zaneschepke.logcatter.model.LogLevel +import com.zaneschepke.logcatter.model.LogMessage +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.io.BufferedReader +import java.io.File +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStreamReader +import java.nio.file.Files +import java.nio.file.Paths +import java.nio.file.StandardOpenOption + +object LogcatUtil { + private const val MAX_FILE_SIZE = 2097152L // 2MB + private const val MAX_FOLDER_SIZE = 10485760L // 10MB + private val findKeyRegex = """[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=""".toRegex() + private val findIpv6AddressRegex = + """(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:) + |{1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:) + |{1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F] + |{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:) + |fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]) + |{0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1 + |{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])) + """.trimMargin().toRegex() + private val findIpv4AddressRegex = """((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}""".toRegex() + private val findTunnelNameRegex = """(?<=tunnel ).*?(?= UP| DOWN)""".toRegex() + private const val CHORE = "Choreographer" + + private object LogcatHelperInit { + var maxFileSize: Long = MAX_FILE_SIZE + var maxFolderSize: Long = MAX_FOLDER_SIZE + var pID: Int = 0 + var publicAppDirectory = "" + var logcatPath = "" + } + + fun init(maxFileSize: Long = MAX_FILE_SIZE, maxFolderSize: Long = MAX_FOLDER_SIZE, context: Context): LocalLogCollector { + if (maxFileSize > maxFolderSize) { + throw IllegalStateException("maxFileSize must be less than maxFolderSize") + } + synchronized(LogcatHelperInit) { + LogcatHelperInit.maxFileSize = maxFileSize + LogcatHelperInit.maxFolderSize = maxFolderSize + LogcatHelperInit.pID = android.os.Process.myPid() + context.getExternalFilesDir(null)?.let { + LogcatHelperInit.publicAppDirectory = it.absolutePath + LogcatHelperInit.logcatPath = + LogcatHelperInit.publicAppDirectory + File.separator + "logs" + val logDirectory = File(LogcatHelperInit.logcatPath) + if (!logDirectory.exists()) { + logDirectory.mkdir() + } + } + return Logcat + } + } + + internal object Logcat : LocalLogCollector { + private var logcatReader: LogcatReader? = null + + override fun start(onLogMessage: ((message: LogMessage) -> Unit)?) { + logcatReader ?: run { + logcatReader = + LogcatReader( + LogcatHelperInit.pID.toString(), + LogcatHelperInit.logcatPath, + onLogMessage, + ) + } + logcatReader?.let { logReader -> + if (!logReader.isAlive) logReader.start() + } + } + + override fun stop() { + logcatReader?.stopLogs() + logcatReader = null + } + + private fun mergeLogsApi26(sourceDir: String, outputFile: File) { + val outputFilePath = Paths.get(outputFile.absolutePath) + val logcatPath = Paths.get(sourceDir) + + Files.list(logcatPath).use { + it.sorted { o1, o2 -> + Files.getLastModifiedTime(o1).compareTo(Files.getLastModifiedTime(o2)) + } + .flatMap(Files::lines).use { lines -> + lines.forEach { line -> + Files.write( + outputFilePath, + (line + System.lineSeparator()).toByteArray(), + StandardOpenOption.CREATE, + StandardOpenOption.APPEND, + ) + } + } + } + } + + override suspend fun getLogFile(): Result { + stop() + return withContext(Dispatchers.IO) { + try { + val outputDir = + File(LogcatHelperInit.publicAppDirectory + File.separator + "output") + val outputFile = File(outputDir.absolutePath + File.separator + "logs.txt") + + if (!outputDir.exists()) outputDir.mkdir() + if (outputFile.exists()) outputFile.delete() + + mergeLogsApi26(LogcatHelperInit.logcatPath, outputFile) + Result.success(outputFile) + } catch (e: Exception) { + Result.failure(e) + } finally { + start() + } + } + } + + private val _bufferedLogs = + MutableSharedFlow( + replay = 10_000, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + + override val bufferedLogs: Flow = _bufferedLogs.asSharedFlow() + + private class LogcatReader( + pID: String, + private val logcatPath: String, + private val callback: ((input: LogMessage) -> Unit)?, + ) : Thread() { + private var logcatProc: Process? = null + private var reader: BufferedReader? = null + private var mRunning = true + private var command = "" + private var clearLogCommand = "" + private var outputStream: FileOutputStream? = null + + init { + try { + outputStream = FileOutputStream(createLogFile(logcatPath)) + } catch (e: FileNotFoundException) { + Timber.e(e) + } + + command = "logcat -v epoch | grep \"($pID)\"" + clearLogCommand = "logcat -c" + } + + fun stopLogs() { + mRunning = false + } + + fun clear() { + Runtime.getRuntime().exec(clearLogCommand) + } + + private fun obfuscator(log: String): String { + return findKeyRegex.replace(log, "").let { first -> + findIpv6AddressRegex.replace(first, "").let { second -> + findTunnelNameRegex.replace(second, "") + } + }.let { last -> findIpv4AddressRegex.replace(last, "") } + } + + override fun run() { + if (outputStream == null) return + try { + clear() + logcatProc = Runtime.getRuntime().exec(command) + reader = BufferedReader(InputStreamReader(logcatProc!!.inputStream), 1024) + var line: String? = null + + while (mRunning && run { + line = reader!!.readLine() + line + } != null + ) { + if (!mRunning) { + break + } + if (line!!.isEmpty()) { + continue + } + + if (outputStream!!.channel.size() >= LogcatHelperInit.maxFileSize) { + outputStream!!.close() + outputStream = FileOutputStream(createLogFile(logcatPath)) + } + if (getFolderSize(logcatPath) >= LogcatHelperInit.maxFolderSize) { + deleteOldestFile(logcatPath) + } + line?.let { text -> + val obfuscated = obfuscator(text) + outputStream!!.write( + (obfuscated + System.lineSeparator()).toByteArray(), + ) + try { + val logMessage = LogMessage.from(obfuscated) + when (logMessage.level) { + LogLevel.VERBOSE -> Unit + else -> { + if (!logMessage.tag.contains(CHORE)) { + _bufferedLogs.tryEmit(logMessage) + } + } + } + callback?.let { + it(logMessage) + } + } catch (e: Exception) { + Timber.e(e) + } + } + } + } catch (e: IOException) { + Timber.e(e) + } finally { + logcatProc?.destroy() + logcatProc = null + + try { + reader?.close() + outputStream?.close() + reader = null + outputStream = null + } catch (e: IOException) { + Timber.e(e) + } + } + } + + private fun getFolderSize(path: String): Long { + File(path).run { + var size = 0L + if (this.isDirectory && this.listFiles() != null) { + for (file in this.listFiles()!!) { + size += getFolderSize(file.absolutePath) + } + } else { + size = this.length() + } + return size + } + } + + private fun createLogFile(dir: String): File { + return File(dir, "logcat_" + System.currentTimeMillis() + ".txt") + } + + private fun deleteOldestFile(path: String) { + val directory = File(path) + if (directory.isDirectory) { + directory.listFiles()?.toMutableList()?.run { + this.sortBy { it.lastModified() } + this.first().delete() + } + } + } + } + } +} diff --git a/logcatter/src/main/java/com/zaneschepke/logcatter/Logcatter.kt b/logcatter/src/main/java/com/zaneschepke/logcatter/Logcatter.kt deleted file mode 100644 index 8496d401..00000000 --- a/logcatter/src/main/java/com/zaneschepke/logcatter/Logcatter.kt +++ /dev/null @@ -1,271 +0,0 @@ -package com.zaneschepke.logcatter - -import android.content.Context -import com.zaneschepke.logcatter.model.LogLevel -import com.zaneschepke.logcatter.model.LogMessage -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.withContext -import timber.log.Timber -import java.io.BufferedReader -import java.io.File -import java.io.FileNotFoundException -import java.io.FileOutputStream -import java.io.IOException -import java.io.InputStreamReader -import java.nio.file.Files -import java.nio.file.Paths -import java.nio.file.StandardOpenOption - -object LogcatHelper { - - private const val MAX_FILE_SIZE = 2097152L // 2MB - private const val MAX_FOLDER_SIZE = 10485760L // 10MB - private val findKeyRegex = """[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=""".toRegex() - private val findIpv6AddressRegex = - """(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))""".toRegex() - private val findIpv4AddressRegex = """((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}""".toRegex() - private val findTunnelNameRegex = """(?<=tunnel ).*?(?= UP| DOWN)""".toRegex() - private const val CHORE = "Choreographer" - - private object LogcatHelperInit { - var maxFileSize: Long = MAX_FILE_SIZE - var maxFolderSize: Long = MAX_FOLDER_SIZE - var pID: Int = 0 - var publicAppDirectory = "" - var logcatPath = "" - } - - fun init( - maxFileSize: Long = MAX_FILE_SIZE, - maxFolderSize: Long = MAX_FOLDER_SIZE, - context: Context - ): LocalLogCollector { - if (maxFileSize > maxFolderSize) { - throw IllegalStateException("maxFileSize must be less than maxFolderSize") - } - synchronized(LogcatHelperInit) { - LogcatHelperInit.maxFileSize = maxFileSize - LogcatHelperInit.maxFolderSize = maxFolderSize - LogcatHelperInit.pID = android.os.Process.myPid() - context.getExternalFilesDir(null)?.let { - LogcatHelperInit.publicAppDirectory = it.absolutePath - LogcatHelperInit.logcatPath = - LogcatHelperInit.publicAppDirectory + File.separator + "logs" - val logDirectory = File(LogcatHelperInit.logcatPath) - if (!logDirectory.exists()) { - logDirectory.mkdir() - } - } - return Logcat - } - } - - internal object Logcat : LocalLogCollector { - - private var logcatReader: LogcatReader? = null - - override fun start(onLogMessage: ((message: LogMessage) -> Unit)?) { - logcatReader ?: run { - logcatReader = LogcatReader( - LogcatHelperInit.pID.toString(), - LogcatHelperInit.logcatPath, - onLogMessage, - ) - } - logcatReader?.let { logReader -> - if (!logReader.isAlive) logReader.start() - } - } - - override fun stop() { - logcatReader?.stopLogs() - logcatReader = null - } - - private fun mergeLogsApi26(sourceDir: String, outputFile: File) { - val outputFilePath = Paths.get(outputFile.absolutePath) - val logcatPath = Paths.get(sourceDir) - - Files.list(logcatPath).use { - it.sorted { o1, o2 -> - Files.getLastModifiedTime(o1).compareTo(Files.getLastModifiedTime(o2)) - } - .flatMap(Files::lines).use { lines -> - lines.forEach { line -> - Files.write( - outputFilePath, - (line + System.lineSeparator()).toByteArray(), - StandardOpenOption.CREATE, - StandardOpenOption.APPEND, - ) - } - } - } - } - - override suspend fun getLogFile(): Result { - stop() - return withContext(Dispatchers.IO) { - try { - val outputDir = - File(LogcatHelperInit.publicAppDirectory + File.separator + "output") - val outputFile = File(outputDir.absolutePath + File.separator + "logs.txt") - - if (!outputDir.exists()) outputDir.mkdir() - if (outputFile.exists()) outputFile.delete() - - mergeLogsApi26(LogcatHelperInit.logcatPath, outputFile) - Result.success(outputFile) - } catch (e: Exception) { - Result.failure(e) - } finally { - start() - } - } - } - - private val _bufferedLogs = MutableSharedFlow( - replay = 10_000, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) - - override val bufferedLogs: Flow = _bufferedLogs.asSharedFlow() - - private class LogcatReader( - pID: String, - private val logcatPath: String, - private val callback: ((input: LogMessage) -> Unit)?, - ) : Thread() { - private var logcatProc: Process? = null - private var reader: BufferedReader? = null - private var mRunning = true - private var command = "" - private var clearLogCommand = "" - private var outputStream: FileOutputStream? = null - - init { - try { - outputStream = FileOutputStream(createLogFile(logcatPath)) - } catch (e: FileNotFoundException) { - Timber.e(e) - } - - command = "logcat -v epoch | grep \"($pID)\"" - clearLogCommand = "logcat -c" - } - - fun stopLogs() { - mRunning = false - } - - fun clear() { - Runtime.getRuntime().exec(clearLogCommand) - } - - private fun obfuscator(log: String): String { - return findKeyRegex.replace(log, "").let { first -> - findIpv6AddressRegex.replace(first, "").let { second -> - findTunnelNameRegex.replace(second, "") - } - }.let { last -> findIpv4AddressRegex.replace(last, "") } - } - - override fun run() { - if (outputStream == null) return - try { - clear() - logcatProc = Runtime.getRuntime().exec(command) - reader = BufferedReader(InputStreamReader(logcatProc!!.inputStream), 1024) - var line: String? = null - - while (mRunning && run { - line = reader!!.readLine() - line - } != null - ) { - if (!mRunning) { - break - } - if (line!!.isEmpty()) { - continue - } - - if (outputStream!!.channel.size() >= LogcatHelperInit.maxFileSize) { - outputStream!!.close() - outputStream = FileOutputStream(createLogFile(logcatPath)) - } - if (getFolderSize(logcatPath) >= LogcatHelperInit.maxFolderSize) { - deleteOldestFile(logcatPath) - } - line?.let { text -> - val obfuscated = obfuscator(text) - outputStream!!.write((obfuscated + System.lineSeparator()).toByteArray()) - try { - val logMessage = LogMessage.from(obfuscated) - when (logMessage.level) { - LogLevel.VERBOSE -> Unit - else -> { - if (!logMessage.tag.contains(CHORE)) { - _bufferedLogs.tryEmit(logMessage) - } - } - } - callback?.let { - it(logMessage) - } - } catch (e: Exception) { - Timber.e(e) - } - } - } - } catch (e: IOException) { - Timber.e(e) - } finally { - logcatProc?.destroy() - logcatProc = null - - try { - reader?.close() - outputStream?.close() - reader = null - outputStream = null - } catch (e: IOException) { - Timber.e(e) - } - } - } - - private fun getFolderSize(path: String): Long { - File(path).run { - var size = 0L - if (this.isDirectory && this.listFiles() != null) { - for (file in this.listFiles()!!) { - size += getFolderSize(file.absolutePath) - } - } else { - size = this.length() - } - return size - } - } - - private fun createLogFile(dir: String): File { - return File(dir, "logcat_" + System.currentTimeMillis() + ".txt") - } - - private fun deleteOldestFile(path: String) { - val directory = File(path) - if (directory.isDirectory) { - directory.listFiles()?.toMutableList()?.run { - this.sortBy { it.lastModified() } - this.first().delete() - } - } - } - } - } -} 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 e427db5e..32befcab 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 5b390189..46bbf6d9 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 903247b6..67c1dfb7 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/settings.gradle.kts b/settings.gradle.kts index 0d52c4f6..15c4e847 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,31 +1,33 @@ pluginManagement { - repositories { - mavenLocal() - google() - mavenCentral() - gradlePluginPortal() - } + repositories { + mavenLocal() + google() + mavenCentral() + gradlePluginPortal() + } } dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - mavenLocal() - google() - mavenCentral() - } + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + mavenLocal() + google() + mavenCentral() + } } fun getLocalProperty(key: String, file: String = "local.properties"): String? { - val properties = java.util.Properties() - val localProperties = File(file) - if (localProperties.isFile) { - java.io.InputStreamReader(java.io.FileInputStream(localProperties), Charsets.UTF_8) - .use { reader -> - properties.load(reader) - } - } else return null - return properties.getProperty(key) + val properties = java.util.Properties() + val localProperties = File(file) + if (localProperties.isFile) { + java.io.InputStreamReader(java.io.FileInputStream(localProperties), Charsets.UTF_8) + .use { reader -> + properties.load(reader) + } + } else { + return null + } + return properties.getProperty(key) } rootProject.name = "WG Tunnel"