From ffa7a207fb77997d9fafaf897f4623e54891b3c0 Mon Sep 17 00:00:00 2001 From: Zane Schepke Date: Wed, 20 Dec 2023 23:13:48 -0500 Subject: [PATCH] feat: add basic kernel support Added basic kernel support to allow users to switch between userspace and kernel wireguard Improved location disclosure flow to only show once once per app install Fix airplane mode bug Improve database migration testing Fix auto-tunneling permission bug. Lint Closes #67 Closes #43 --- .github/workflows/android.yml | 2 +- app/build.gradle.kts | 86 ++- app/fdroid-rules.pro | 6 +- app/proguard-rules.pro | 3 + .../4.json | 154 ++++ .../ExampleInstrumentedTest.kt | 2 +- .../wireguardautotunnel/MigrationTest.kt | 63 ++ app/src/main/AndroidManifest.xml | 9 +- .../wireguardautotunnel/Constants.kt | 5 +- .../wireguardautotunnel/Extensions.kt | 10 +- .../WireGuardAutoTunnel.kt | 30 +- .../module/DatabaseModule.kt | 10 +- .../wireguardautotunnel/module/Kernel.kt | 7 + .../module/RepositoryModule.kt | 16 +- .../module/ServiceModule.kt | 15 +- .../module/TunnelModule.kt | 36 +- .../wireguardautotunnel/module/Userspace.kt | 7 + .../receiver/BootReceiver.kt | 12 +- .../receiver/NotificationActionReceiver.kt | 13 +- .../repository/AppDatabase.kt | 19 +- .../repository/DatabaseListConverters.kt | 7 +- .../repository/SettingsDoa.kt | 3 +- .../repository/TunnelConfigDao.kt | 5 +- .../repository/datastore/DataStoreManager.kt | 39 + .../repository/model/Settings.kt | 45 +- .../repository/model/TunnelConfig.kt | 17 +- .../service/foreground/Action.kt | 2 +- .../service/foreground/ForegroundService.kt | 15 +- .../service/foreground/ServiceManager.kt | 109 ++- .../service/foreground/ServiceState.kt | 2 +- .../WireGuardConnectivityWatcherService.kt | 199 +++-- .../foreground/WireGuardTunnelService.kt | 195 ++--- .../service/network/BaseNetworkService.kt | 137 ++-- .../service/network/EthernetService.kt | 9 +- .../service/network/MobileDataService.kt | 9 +- .../service/network/NetworkService.kt | 1 + .../service/network/NetworkStatus.kt | 9 +- .../service/network/WifiService.kt | 9 +- .../notification/NotificationService.kt | 4 +- .../notification/WireGuardNotification.kt | 48 +- .../service/shortcut/ShortcutsActivity.kt | 53 +- .../service/tile/TunnelControlTile.kt | 81 +- .../service/tunnel/HandshakeStatus.kt | 11 +- .../service/tunnel/VpnService.kt | 19 +- .../service/tunnel/WireGuardTunnel.kt | 147 ++-- .../ui/ActivityViewModel.kt | 8 + .../ui/CaptureActivityPortrait.kt | 2 +- .../wireguardautotunnel/ui/MainActivity.kt | 155 ++-- .../wireguardautotunnel/ui/Routes.kt | 39 +- .../ui/common/ClickableIconButton.kt | 22 +- .../common/PermissionRequestFailedScreen.kt | 18 +- .../ui/common/RowListItem.kt | 39 +- .../ui/common/SearchBar.kt | 14 +- .../ui/common/config/ConfigurationTextBox.kt | 19 +- .../ui/common/config/ConfigurationToggle.kt | 15 +- .../ui/common/navigation/BottomNavBar.kt | 14 +- .../ui/common/prompt/AuthorizationPrompt.kt | 124 ++-- .../ui/common/prompt/CustomSnackbar.kt | 13 +- .../ui/common/text/SectionTitle.kt | 7 +- .../ui/models/InterfaceProxy.kt | 27 +- .../ui/models/PeerProxy.kt | 53 +- .../ui/screens/config/ConfigScreen.kt | 266 ++++--- .../ui/screens/config/ConfigViewModel.kt | 252 ++++--- .../ui/screens/main/MainScreen.kt | 395 ++++++---- .../ui/screens/main/MainViewModel.kt | 63 +- .../ui/screens/settings/SettingsScreen.kt | 696 ++++++++++-------- .../ui/screens/settings/SettingsViewModel.kt | 119 ++- .../ui/screens/support/SupportScreen.kt | 244 ++++-- .../ui/screens/support/SupportViewModel.kt | 25 + .../wireguardautotunnel/ui/theme/Color.kt | 3 +- .../wireguardautotunnel/ui/theme/Theme.kt | 73 +- .../ui/theme/TransparentSystemBars.kt | 2 +- .../wireguardautotunnel/ui/theme/Type.kt | 50 +- .../util/{StorageUtil.kt => FileUtils.kt} | 45 +- .../wireguardautotunnel/util/NumberUtils.kt | 11 +- .../util/WgTunnelException.kt | 9 +- app/src/main/res/values/strings.xml | 3 +- .../wireguardautotunnel/ExampleUnitTest.kt | 2 +- build.gradle.kts | 2 +- buildSrc/build.gradle.kts | 2 +- buildSrc/src/main/kotlin/BuildHelper.kt | 41 +- buildSrc/src/main/kotlin/Constants.kt | 6 +- .../android/en-US/changelogs/32400.txt | 5 + gradle/libs.versions.toml | 9 +- index.html | 47 -- settings.gradle.kts | 1 - 86 files changed, 2930 insertions(+), 1690 deletions(-) create mode 100644 app/schemas/com.zaneschepke.wireguardautotunnel.repository.AppDatabase/4.json create mode 100644 app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/MigrationTest.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/module/Kernel.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/module/Userspace.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/datastore/DataStoreManager.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/ActivityViewModel.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportViewModel.kt rename app/src/main/java/com/zaneschepke/wireguardautotunnel/util/{StorageUtil.kt => FileUtils.kt} (54%) create mode 100644 fastlane/metadata/android/en-US/changelogs/32400.txt delete mode 100644 index.html diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index a42d733..6f928bd 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -70,7 +70,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: # fix hardcode changelog file name - body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/32300.txt + body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/32400.txt tag_name: ${{ github.ref_name }} name: Release ${{ github.ref_name }} draft: false diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e47f6d4..ae8e5ac 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -23,6 +23,10 @@ android { arg("room.schemaLocation", "$projectDir/schemas") } + sourceSets { + getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room + } + resourceConfigurations.addAll(listOf("en")) testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -33,33 +37,51 @@ android { signingConfigs { create(Constants.RELEASE) { - val properties = Properties().apply { - //created local file for signing details - try { - load(file("signing.properties").reader()) - } catch (_ : Exception) { - load(file("signing_template.properties").reader()) + val properties = + Properties().apply { + // created local file for signing details + try { + load(file("signing.properties").reader()) + } catch (_: Exception) { + load(file("signing_template.properties").reader()) + } } - } - //try to get secrets from env first for pipeline build, then properties file for local build - storeFile = file(System.getenv().getOrDefault(Constants.KEY_STORE_PATH_VAR, properties.getProperty(Constants.KEY_STORE_PATH_VAR))) - storePassword = System.getenv().getOrDefault(Constants.STORE_PASS_VAR, properties.getProperty(Constants.STORE_PASS_VAR)) - keyAlias = System.getenv().getOrDefault(Constants.KEY_ALIAS_VAR, properties.getProperty(Constants.KEY_ALIAS_VAR)) - keyPassword = System.getenv().getOrDefault(Constants.KEY_PASS_VAR, properties.getProperty(Constants.KEY_PASS_VAR)) + // try to get secrets from env first for pipeline build, then properties file for local build + storeFile = file( + System.getenv().getOrDefault( + Constants.KEY_STORE_PATH_VAR, + properties.getProperty(Constants.KEY_STORE_PATH_VAR) + ) + ) + storePassword = System.getenv().getOrDefault( + Constants.STORE_PASS_VAR, + properties.getProperty(Constants.STORE_PASS_VAR) + ) + keyAlias = System.getenv().getOrDefault( + Constants.KEY_ALIAS_VAR, + properties.getProperty(Constants.KEY_ALIAS_VAR) + ) + keyPassword = System.getenv().getOrDefault( + Constants.KEY_PASS_VAR, + properties.getProperty(Constants.KEY_PASS_VAR) + ) } } buildTypes { - //don't strip - packaging.jniLibs.keepDebugSymbols.addAll(listOf("libwg-go.so", "libwg-quick.so", "libwg.so")) + // don't strip + packaging.jniLibs.keepDebugSymbols.addAll( + listOf("libwg-go.so", "libwg-quick.so", "libwg.so") + ) 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" + val outputFileName = + "${Constants.APP_NAME}-${variant.flavorName}-${variant.buildType.name}-${variant.versionName}.apk" output.outputFileName = outputFileName } } @@ -85,8 +107,7 @@ android { } create("general") { dimension = Constants.TYPE - if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle)) - { + if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle)) { apply(plugin = "com.google.gms.google-services") apply(plugin = "com.google.firebase.crashlytics") } @@ -103,7 +124,6 @@ android { buildFeatures { compose = true buildConfig = true - } composeOptions { kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get() @@ -129,20 +149,22 @@ dependencies { implementation(libs.androidx.material3) implementation(libs.androidx.appcompat) - //test + // 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) - //wg + // wg implementation(libs.tunnel) coreLibraryDesugaring(libs.desugar.jdk.libs) - //logging + // logging implementation(libs.timber) // compose navigation @@ -153,41 +175,41 @@ dependencies { implementation(libs.hilt.android) ksp(libs.hilt.android.compiler) - //accompanist + // accompanist implementation(libs.accompanist.systemuicontroller) implementation(libs.accompanist.permissions) implementation(libs.accompanist.flowlayout) implementation(libs.accompanist.drawablepainter) - //room + // storage implementation(libs.androidx.room.runtime) ksp(libs.androidx.room.compiler) implementation(libs.androidx.room.ktx) + implementation(libs.androidx.datastore.preferences) - //lifecycle + // lifecycle implementation(libs.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.process) - - //icons + // icons implementation(libs.material.icons.extended) - //serialization + // serialization implementation(libs.kotlinx.serialization.json) - //firebase crashlytics + // firebase crashlytics generalImplementation(platform(libs.firebase.bom)) generalImplementation(libs.google.firebase.crashlytics.ktx) generalImplementation(libs.google.firebase.analytics.ktx) - //barcode scanning + // barcode scanning implementation(libs.zxing.android.embedded) implementation(libs.zxing.core) - //bio + // bio implementation(libs.androidx.biometric.ktx) - //shortcuts + // shortcuts implementation(libs.androidx.core) implementation(libs.androidx.core.google.shortcuts) -} \ No newline at end of file +} diff --git a/app/fdroid-rules.pro b/app/fdroid-rules.pro index e5480ee..86f6534 100644 --- a/app/fdroid-rules.pro +++ b/app/fdroid-rules.pro @@ -1 +1,5 @@ --dontwarn com.google.errorprone.annotations.** \ No newline at end of file +-dontwarn com.google.errorprone.annotations.** + +-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite { + ; +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index f1b4245..2c8662e 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -19,3 +19,6 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile +-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite { + ; +} diff --git a/app/schemas/com.zaneschepke.wireguardautotunnel.repository.AppDatabase/4.json b/app/schemas/com.zaneschepke.wireguardautotunnel.repository.AppDatabase/4.json new file mode 100644 index 0000000..3795fe6 --- /dev/null +++ b/app/schemas/com.zaneschepke.wireguardautotunnel.repository.AppDatabase/4.json @@ -0,0 +1,154 @@ +{ + "formatVersion": 1, + "database": { + "version": 4, + "identityHash": "aee55639422df8dadfe74c3bad204477", + "entities": [ + { + "tableName": "Settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `default_tunnel` TEXT, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_battery_saver_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAutoTunnelEnabled", + "columnName": "is_tunnel_enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isTunnelOnMobileDataEnabled", + "columnName": "is_tunnel_on_mobile_data_enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "trustedNetworkSSIDs", + "columnName": "trusted_network_ssids", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultTunnel", + "columnName": "default_tunnel", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isAlwaysOnVpnEnabled", + "columnName": "is_always_on_vpn_enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isTunnelOnEthernetEnabled", + "columnName": "is_tunnel_on_ethernet_enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isShortcutsEnabled", + "columnName": "is_shortcuts_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isBatterySaverEnabled", + "columnName": "is_battery_saver_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isTunnelOnWifiEnabled", + "columnName": "is_tunnel_on_wifi_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isKernelEnabled", + "columnName": "is_kernel_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isRestoreOnBootEnabled", + "columnName": "is_restore_on_boot_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isMultiTunnelEnabled", + "columnName": "is_multi_tunnel_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TunnelConfig", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wgQuick", + "columnName": "wg_quick", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_TunnelConfig_name", + "unique": true, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'aee55639422df8dadfe74c3bad204477')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/ExampleInstrumentedTest.kt index 52139b4..340f904 100644 --- a/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/ExampleInstrumentedTest.kt @@ -19,4 +19,4 @@ class ExampleInstrumentedTest { val appContext = InstrumentationRegistry.getInstrumentation().targetContext assertEquals("com.zaneschepke.wireguardautotunnel", appContext.packageName) } -} \ No newline at end of file +} diff --git a/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/MigrationTest.kt b/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/MigrationTest.kt new file mode 100644 index 0000000..cfc4772 --- /dev/null +++ b/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/MigrationTest.kt @@ -0,0 +1,63 @@ +package com.zaneschepke.wireguardautotunnel + +import androidx.room.testing.MigrationTestHelper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.zaneschepke.wireguardautotunnel.repository.AppDatabase +import java.io.IOException +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class MigrationTest { + private val dbName = "migration-test" + + @get:Rule + val helper: MigrationTestHelper = MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java + ) + + @Test + @Throws(IOException::class) + fun migrate2To3() { + helper.createDatabase(dbName, 3).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( + "INSERT INTO Settings (is_tunnel_enabled, " + + "is_tunnel_on_mobile_data_enabled," + + "trusted_network_ssids," + + "default_tunnel, " + + "is_always_on_vpn_enabled," + + "is_tunnel_on_ethernet_enabled," + + "is_shortcuts_enabled," + + "is_battery_saver_enabled," + + "is_tunnel_on_wifi_enabled)" + + " VALUES (" + + "false," + + "false," + + "'[trustedSSID1,trustedSSID2]'," + + "'defaultTunnel'," + + "false," + + "false," + + "false," + + "false," + + "false)" + ) + execSQL( + "INSERT INTO TunnelConfig (name, wg_quick)" + + " VALUES ('hello', 'hello')" + ) + // 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, 4, true) + // MigrationTestHelper automatically verifies the schema changes, + // but you need to validate that the data was migrated properly. + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bd35721..d01d137 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,6 +17,10 @@ + + + + @@ -102,7 +106,7 @@ android:permission="android.permission.BIND_VPN_SERVICE" android:enabled="true" android:persistent="true" - android:foregroundServiceType="remoteMessaging" + android:foregroundServiceType="systemExempted" android:exported="false"> @@ -115,8 +119,7 @@ android:enabled="true" android:stopWithTask="false" android:persistent="true" - android:foregroundServiceType="location" - android:permission="" + android:foregroundServiceType="systemExempted" android:exported="false"> @Binds @ServiceScoped - abstract fun provideWifiService(wifiService: WifiService) : NetworkService + abstract fun provideMobileDataService(mobileDataService: MobileDataService): NetworkService @Binds @ServiceScoped - abstract fun provideMobileDataService(mobileDataService : MobileDataService) : NetworkService - - @Binds - @ServiceScoped - abstract fun provideEthernetService(ethernetService: EthernetService) : NetworkService -} \ No newline at end of file + 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 62ddac8..7da9de2 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt @@ -3,6 +3,10 @@ package com.zaneschepke.wireguardautotunnel.module import android.content.Context import com.wireguard.android.backend.Backend import com.wireguard.android.backend.GoBackend +import com.wireguard.android.backend.WgQuickBackend +import com.wireguard.android.util.RootShell +import com.wireguard.android.util.ToolsInstaller +import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel import dagger.Module @@ -15,16 +19,40 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) class TunnelModule { + @Provides + @Singleton + fun provideRootShell( + @ApplicationContext context: Context + ): RootShell { + return RootShell(context) + } @Provides @Singleton - fun provideBackend(@ApplicationContext context : Context) : Backend { + @Userspace + fun provideUserspaceBackend( + @ApplicationContext context: Context + ): Backend { return GoBackend(context) } @Provides @Singleton - fun provideVpnService(backend: Backend) : VpnService { - return WireGuardTunnel(backend) + @Kernel + fun provideKernelBackend( + @ApplicationContext context: Context, + rootShell: RootShell + ): Backend { + return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell)) } -} \ No newline at end of file + + @Provides + @Singleton + fun provideVpnService( + @Userspace userspaceBackend: Backend, + @Kernel kernelBackend: Backend, + settingsDoa: SettingsDoa + ): VpnService { + return WireGuardTunnel(userspaceBackend, kernelBackend, settingsDoa) + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/Userspace.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/Userspace.kt new file mode 100644 index 0000000..8a85a7d --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/Userspace.kt @@ -0,0 +1,7 @@ +package com.zaneschepke.wireguardautotunnel.module + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class Userspace diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt index 9748e7b..6e037aa 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt @@ -7,16 +7,18 @@ import com.zaneschepke.wireguardautotunnel.goAsync import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.cancel import javax.inject.Inject +import kotlinx.coroutines.cancel @AndroidEntryPoint class BootReceiver : BroadcastReceiver() { - @Inject - lateinit var settingsRepo : SettingsDoa + lateinit var settingsRepo: SettingsDoa - override fun onReceive(context: Context, intent: Intent) = goAsync { + override fun onReceive( + context: Context, + intent: Intent + ) = goAsync { if (intent.action == Intent.ACTION_BOOT_COMPLETED) { try { val settings = settingsRepo.getAll() @@ -31,4 +33,4 @@ class BootReceiver : BroadcastReceiver() { } } } -} \ No newline at end of file +} 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 f9bb3e3..66adb88 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt @@ -8,16 +8,19 @@ import com.zaneschepke.wireguardautotunnel.goAsync import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject import kotlinx.coroutines.cancel import kotlinx.coroutines.delay -import javax.inject.Inject @AndroidEntryPoint class NotificationActionReceiver : BroadcastReceiver() { - @Inject - lateinit var settingsRepo : SettingsDoa - override fun onReceive(context: Context, intent: Intent?) = goAsync { + lateinit var settingsRepo: SettingsDoa + + override fun onReceive( + context: Context, + intent: Intent? + ) = goAsync { try { val settings = settingsRepo.getAll() if (settings.isNotEmpty()) { @@ -32,4 +35,4 @@ class NotificationActionReceiver : BroadcastReceiver() { cancel() } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/AppDatabase.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/AppDatabase.kt index e56968f..7b83086 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/AppDatabase.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/AppDatabase.kt @@ -7,11 +7,20 @@ import androidx.room.TypeConverters import com.zaneschepke.wireguardautotunnel.repository.model.Settings import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig -@Database(entities = [Settings::class, TunnelConfig::class], version = 3, autoMigrations = [ - AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3) -], exportSchema = true) +@Database( + entities = [Settings::class, TunnelConfig::class], + version = 4, + autoMigrations = [ + AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3), AutoMigration( + from = 3, + to = 4 + ) + ], + exportSchema = true +) @TypeConverters(DatabaseListConverters::class) abstract class AppDatabase : RoomDatabase() { abstract fun settingDao(): SettingsDoa - abstract fun tunnelConfigDoa() : TunnelConfigDao -} \ No newline at end of file + + abstract fun tunnelConfigDoa(): TunnelConfigDao +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/DatabaseListConverters.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/DatabaseListConverters.kt index 21c44d9..d6c2af9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/DatabaseListConverters.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/DatabaseListConverters.kt @@ -9,15 +9,16 @@ class DatabaseListConverters { fun listToString(value: MutableList): String { return Json.encodeToString(value) } + @TypeConverter fun stringToList(value: String): MutableList { - if(value.isEmpty()) return mutableListOf() + if (value.isEmpty()) return mutableListOf() return try { Json.decodeFromString>(value) - } catch (e : Exception) { + } catch (e: Exception) { val list = value.split(",").toMutableList() val json = listToString(list) Json.decodeFromString>(json) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/SettingsDoa.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/SettingsDoa.kt index 49120a3..fbf116a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/SettingsDoa.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/SettingsDoa.kt @@ -10,7 +10,6 @@ import kotlinx.coroutines.flow.Flow @Dao interface SettingsDoa { - @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: Settings) @@ -31,4 +30,4 @@ interface SettingsDoa { @Query("SELECT COUNT('id') FROM settings") suspend fun count(): Long -} \ No newline at end of file +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/TunnelConfigDao.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/TunnelConfigDao.kt index 2533c7a..9040fca 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/TunnelConfigDao.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/TunnelConfigDao.kt @@ -9,8 +9,7 @@ import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig import kotlinx.coroutines.flow.Flow @Dao -interface TunnelConfigDao{ - +interface TunnelConfigDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: TunnelConfig) @@ -31,4 +30,4 @@ interface TunnelConfigDao{ @Query("SELECT * FROM tunnelconfig") fun getAllFlow(): Flow> -} \ No newline at end of file +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/datastore/DataStoreManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/datastore/DataStoreManager.kt new file mode 100644 index 0000000..f282c8f --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/datastore/DataStoreManager.kt @@ -0,0 +1,39 @@ +package com.zaneschepke.wireguardautotunnel.repository.datastore +import android.content.Context +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map + +class DataStoreManager(private val context: Context) { + companion object { + val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN") + } + + // preferences + private val preferencesKey = "preferences" + private val Context.dataStore by preferencesDataStore( + name = preferencesKey + ) + + suspend fun init() { + context.dataStore.data.first() + } + + suspend fun saveToDataStore(key: Preferences.Key, value: T) = + context.dataStore.edit { + it[key] = value + } + + fun getFromStore(key: Preferences.Key) = + context.dataStore.data.map { + it[key] + } + + val locationDisclosureFlow: Flow = context.dataStore.data.map { + it[LOCATION_DISCLOSURE_SHOWN] + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/model/Settings.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/model/Settings.kt index 77bcb3f..c44ace0 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/model/Settings.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/model/Settings.kt @@ -6,18 +6,39 @@ import androidx.room.PrimaryKey @Entity data class Settings( - @PrimaryKey(autoGenerate = true) val id : Int = 0, - @ColumnInfo(name = "is_tunnel_enabled") var isAutoTunnelEnabled : Boolean = false, - @ColumnInfo(name = "is_tunnel_on_mobile_data_enabled") var isTunnelOnMobileDataEnabled : Boolean = false, - @ColumnInfo(name = "trusted_network_ssids") var trustedNetworkSSIDs : MutableList = mutableListOf(), - @ColumnInfo(name = "default_tunnel") var defaultTunnel : String? = null, - @ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled : Boolean = false, - @ColumnInfo(name = "is_tunnel_on_ethernet_enabled") var isTunnelOnEthernetEnabled : Boolean = false, - @ColumnInfo(name = "is_shortcuts_enabled", defaultValue = "false") var isShortcutsEnabled : Boolean = false, - @ColumnInfo(name = "is_battery_saver_enabled", defaultValue = "false") var isBatterySaverEnabled : Boolean = false, - @ColumnInfo(name = "is_tunnel_on_wifi_enabled", defaultValue = "false") var isTunnelOnWifiEnabled : Boolean = false, + @PrimaryKey(autoGenerate = true) val id: Int = 0, + @ColumnInfo(name = "is_tunnel_enabled") var isAutoTunnelEnabled: Boolean = false, + @ColumnInfo(name = "is_tunnel_on_mobile_data_enabled") var isTunnelOnMobileDataEnabled: Boolean = false, + @ColumnInfo(name = "trusted_network_ssids") var trustedNetworkSSIDs: MutableList = mutableListOf(), + @ColumnInfo(name = "default_tunnel") var defaultTunnel: String? = null, + @ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled: Boolean = false, + @ColumnInfo(name = "is_tunnel_on_ethernet_enabled") var isTunnelOnEthernetEnabled: Boolean = false, + @ColumnInfo( + name = "is_shortcuts_enabled", + defaultValue = "false" + ) var isShortcutsEnabled: Boolean = false, + @ColumnInfo( + name = "is_battery_saver_enabled", + defaultValue = "false" + ) var isBatterySaverEnabled: Boolean = false, + @ColumnInfo( + name = "is_tunnel_on_wifi_enabled", + defaultValue = "false" + ) var isTunnelOnWifiEnabled: Boolean = false, + @ColumnInfo( + name = "is_kernel_enabled", + defaultValue = "false" + ) var isKernelEnabled: Boolean = false, + @ColumnInfo( + name = "is_restore_on_boot_enabled", + defaultValue = "false" + ) var isRestoreOnBootEnabled: Boolean = false, + @ColumnInfo( + name = "is_multi_tunnel_enabled", + defaultValue = "false" + ) var isMultiTunnelEnabled: Boolean = false ) { - fun isTunnelConfigDefault(tunnelConfig: TunnelConfig) : Boolean { + fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean { return if (defaultTunnel != null) { val defaultConfig = TunnelConfig.from(defaultTunnel!!) (tunnelConfig.id == defaultConfig.id) @@ -25,4 +46,4 @@ data class Settings( false } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/model/TunnelConfig.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/model/TunnelConfig.kt index 003bd2f..2234368 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/model/TunnelConfig.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/model/TunnelConfig.kt @@ -5,31 +5,30 @@ import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey import com.wireguard.config.Config +import java.io.InputStream import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json -import java.io.InputStream @Entity(indices = [Index(value = ["name"], unique = true)]) @Serializable data class TunnelConfig( - @PrimaryKey(autoGenerate = true) val id : Int = 0, - @ColumnInfo(name = "name") var name : String, - @ColumnInfo(name = "wg_quick") var wgQuick : String, -){ - + @PrimaryKey(autoGenerate = true) val id: Int = 0, + @ColumnInfo(name = "name") var name: String, + @ColumnInfo(name = "wg_quick") var wgQuick: String +) { override fun toString(): String { return Json.encodeToString(serializer(), this) } companion object { - - fun from(string : String) : TunnelConfig { + fun from(string: String): TunnelConfig { return Json.decodeFromString(string) } + fun configFromQuick(wgQuick: String): Config { val inputStream: InputStream = wgQuick.byteInputStream() val reader = inputStream.bufferedReader(Charsets.UTF_8) return Config.parse(reader) } } -} \ No newline at end of file +} 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 e76f7ae..e68a61c 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 @@ -4,4 +4,4 @@ enum class Action { START, START_FOREGROUND, STOP -} \ No newline at end of file +} 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 8fc90b9..0814e91 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 @@ -6,9 +6,7 @@ import android.os.IBinder import androidx.lifecycle.LifecycleService import timber.log.Timber - open class ForegroundService : LifecycleService() { - private var isServiceStarted = false override fun onBind(intent: Intent): IBinder? { @@ -17,7 +15,11 @@ open class ForegroundService : LifecycleService() { return null } - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + override fun onStartCommand( + intent: Intent?, + flags: Int, + startId: Int + ): Int { super.onStartCommand(intent, flags, startId) Timber.d("onStartCommand executed with startId: $startId") if (intent != null) { @@ -41,19 +43,18 @@ open class ForegroundService : LifecycleService() { return START_STICKY } - override fun onDestroy() { super.onDestroy() Timber.d("The service has been destroyed") } - protected open fun startService(extras : Bundle?) { + protected open fun startService(extras: Bundle?) { if (isServiceStarted) return Timber.d("Starting ${this.javaClass.simpleName}") isServiceStarted = true } - protected open fun stopService(extras : Bundle?) { + protected open fun stopService(extras: Bundle?) { Timber.d("Stopping ${this.javaClass.simpleName}") try { stopForeground(STOP_FOREGROUND_REMOVE) @@ -63,4 +64,4 @@ open class ForegroundService : LifecycleService() { } isServiceStarted = false } -} \ No newline at end of file +} 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 b57cb7e..1d89552 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 @@ -16,44 +16,60 @@ object ServiceManager { .getRunningServices(Integer.MAX_VALUE) .any { it.service.className == service.name } - fun getServiceState(context: Context, cls : Class): ServiceState { + fun getServiceState( + context: Context, + cls: Class + ): ServiceState { val isServiceRunning = context.isServiceRunning(cls) - return if(isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED + return if (isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED } - private fun actionOnService(action: Action, context: Context, cls : Class, extras : Map? = null) { + private fun actionOnService( + action: Action, + context: Context, + cls: Class, + extras: Map? = null + ) { if (getServiceState(context, cls) == ServiceState.STOPPED && action == Action.STOP) return if (getServiceState(context, cls) == ServiceState.STARTED && action == Action.START) return - val intent = Intent(context, cls).also { - it.action = action.name - extras?.forEach {(k, v) -> - it.putExtra(k, v) + val intent = + Intent(context, cls).also { + it.action = action.name + extras?.forEach { (k, v) -> + it.putExtra(k, v) + } } - } intent.component?.javaClass try { - when(action) { + when (action) { Action.START_FOREGROUND -> { context.startForegroundService(intent) } + Action.START -> { context.startService(intent) } + Action.STOP -> context.startService(intent) } - } catch (e : Exception) { + } catch (e: Exception) { Timber.e(e.message) } } - fun startVpnService(context : Context, tunnelConfig : String) { + fun startVpnService( + context: Context, + tunnelConfig: String + ) { actionOnService( Action.START, context, WireGuardTunnelService::class.java, - mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig)) + mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig) + ) } - fun stopVpnService(context : Context) { + + fun stopVpnService(context: Context) { actionOnService( Action.STOP, context, @@ -61,41 +77,70 @@ object ServiceManager { ) } - fun startVpnServiceForeground(context : Context, tunnelConfig : String) { + fun startVpnServiceForeground( + context: Context, + tunnelConfig: String + ) { actionOnService( Action.START_FOREGROUND, context, WireGuardTunnelService::class.java, - mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig)) + mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig) + ) } - private fun startWatcherServiceForeground(context : Context, tunnelConfig : String) { + private fun startWatcherServiceForeground( + context: Context, + tunnelConfig: String + ) { actionOnService( - Action.START, context, - WireGuardConnectivityWatcherService::class.java, mapOf(context. - getString(R.string.tunnel_extras_key) to - tunnelConfig)) + Action.START, + context, + WireGuardConnectivityWatcherService::class.java, + mapOf( + context + .getString(R.string.tunnel_extras_key) to + tunnelConfig + ) + ) } - fun startWatcherService(context : Context, tunnelConfig : String) { + fun startWatcherService( + context: Context, + tunnelConfig: String + ) { actionOnService( - Action.START, context, - WireGuardConnectivityWatcherService::class.java, mapOf(context. - getString(R.string.tunnel_extras_key) to - tunnelConfig)) + Action.START, + context, + WireGuardConnectivityWatcherService::class.java, + mapOf( + context + .getString(R.string.tunnel_extras_key) to + tunnelConfig + ) + ) } - fun stopWatcherService(context : Context) { + fun stopWatcherService(context: Context) { actionOnService( - Action.STOP, context, - WireGuardConnectivityWatcherService::class.java) + Action.STOP, + context, + WireGuardConnectivityWatcherService::class.java + ) } - fun toggleWatcherServiceForeground(context: Context, tunnelConfig : String) { - when(getServiceState( context, - WireGuardConnectivityWatcherService::class.java,)) { + fun toggleWatcherServiceForeground( + context: Context, + tunnelConfig: String + ) { + when ( + getServiceState( + context, + WireGuardConnectivityWatcherService::class.java + ) + ) { ServiceState.STARTED -> stopWatcherService(context) ServiceState.STOPPED -> startWatcherServiceForeground(context, tunnelConfig) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceState.kt index 2671b2c..3cddee8 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceState.kt @@ -3,4 +3,4 @@ package com.zaneschepke.wireguardautotunnel.service.foreground enum class ServiceState { STARTED, STOPPED, -} \ No newline at end of file +} 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 de1c962..3a2f7f9 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 @@ -7,6 +7,7 @@ import android.content.Intent import android.os.Bundle import android.os.PowerManager import android.os.SystemClock +import androidx.core.app.ServiceCompat import androidx.lifecycle.lifecycleScope import com.wireguard.android.backend.Tunnel import com.zaneschepke.wireguardautotunnel.Constants @@ -21,17 +22,16 @@ import com.zaneschepke.wireguardautotunnel.service.network.WifiService import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber -import javax.inject.Inject @AndroidEntryPoint class WireGuardConnectivityWatcherService : ForegroundService() { - private val foregroundId = 122 @Inject @@ -64,30 +64,37 @@ class WireGuardConnectivityWatcherService : ForegroundService() { private var wakeLock: PowerManager.WakeLock? = null private val tag = this.javaClass.name - override fun onCreate() { super.onCreate() lifecycleScope.launch(Dispatchers.Main) { - launchWatcherNotification() + try { + launchWatcherNotification() + } catch (e: Exception) { + Timber.e("Failed to start watcher service, not enough permissions") + } } } override fun startService(extras: Bundle?) { super.startService(extras) - launchWatcherNotification() - val tunnelId = extras?.getString(getString(R.string.tunnel_extras_key)) - if (tunnelId != null) { - this.tunnelConfig = tunnelId - } - // we need this lock so our service gets not affected by Doze Mode - lifecycleScope.launch { - initWakeLock() - } - cancelWatcherJob() - if (this::tunnelConfig.isInitialized) { - startWatcherJob() - } else { - stopService(extras) + try { + launchWatcherNotification() + val tunnelId = extras?.getString(getString(R.string.tunnel_extras_key)) + if (tunnelId != null) { + this.tunnelConfig = tunnelId + } + // we need this lock so our service gets not affected by Doze Mode + lifecycleScope.launch { + initWakeLock() + } + cancelWatcherJob() + if (this::tunnelConfig.isInitialized) { + startWatcherJob() + } else { + stopService(extras) + } + } catch (e: Exception) { + Timber.e("Failed to launch watcher service, no permissions") } } @@ -103,23 +110,32 @@ class WireGuardConnectivityWatcherService : ForegroundService() { } private fun launchWatcherNotification() { - val notification = notificationService.createNotification( - channelId = getString(R.string.watcher_channel_id), - channelName = getString(R.string.watcher_channel_name), - description = getString(R.string.watcher_notification_text), - vibration = false + val notification = + notificationService.createNotification( + channelId = getString(R.string.watcher_channel_id), + channelName = getString(R.string.watcher_channel_name), + description = getString(R.string.watcher_notification_text), + vibration = false + ) + ServiceCompat.startForeground( + this, + foregroundId, + notification, + Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID ) - super.startForeground(foregroundId, notification) } - //try to start task again if killed + // try to start task again if killed override fun onTaskRemoved(rootIntent: Intent) { Timber.d("Task Removed called") val restartServiceIntent = Intent(rootIntent) - val restartServicePendingIntent: PendingIntent = PendingIntent.getService( - this, 1, restartServiceIntent, - PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE - ) + val restartServicePendingIntent: PendingIntent = + PendingIntent.getService( + this, + 1, + restartServiceIntent, + PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE + ) applicationContext.getSystemService(Context.ALARM_SERVICE) val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager @@ -131,9 +147,10 @@ class WireGuardConnectivityWatcherService : ForegroundService() { } private suspend fun initWakeLock() { - val isBatterySaverOn = withContext(lifecycleScope.coroutineContext) { - settingsRepo.getAll().firstOrNull()?.isBatterySaverEnabled ?: false - } + val isBatterySaverOn = + withContext(lifecycleScope.coroutineContext) { + settingsRepo.getAll().firstOrNull()?.isBatterySaverEnabled ?: false + } wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).run { newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply { @@ -155,28 +172,29 @@ class WireGuardConnectivityWatcherService : ForegroundService() { } private fun startWatcherJob() { - watcherJob = lifecycleScope.launch(Dispatchers.IO) { - val settings = settingsRepo.getAll() - if (settings.isNotEmpty()) { - setting = settings[0] - } - launch { - watchForWifiConnectivityChanges() - } - if (setting.isTunnelOnMobileDataEnabled) { + watcherJob = + lifecycleScope.launch(Dispatchers.IO) { + val settings = settingsRepo.getAll() + if (settings.isNotEmpty()) { + setting = settings[0] + } launch { - watchForMobileDataConnectivityChanges() + watchForWifiConnectivityChanges() + } + if (setting.isTunnelOnMobileDataEnabled) { + launch { + watchForMobileDataConnectivityChanges() + } + } + if (setting.isTunnelOnEthernetEnabled) { + launch { + watchForEthernetConnectivityChanges() + } + } + launch { + manageVpn() } } - if (setting.isTunnelOnEthernetEnabled) { - launch { - watchForEthernetConnectivityChanges() - } - } - launch { - manageVpn() - } - } } private suspend fun watchForMobileDataConnectivityChanges() { @@ -232,7 +250,9 @@ class WireGuardConnectivityWatcherService : ForegroundService() { is NetworkStatus.CapabilitiesChanged -> { Timber.d("Wifi capabilities changed") isWifiConnected = true - currentNetworkSSID = wifiService.getNetworkName(it.networkCapabilities) ?: "" + val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: "" + Timber.d("Detected SSID: $ssid") + currentNetworkSSID = ssid } is NetworkStatus.Unavailable -> { @@ -246,49 +266,70 @@ class WireGuardConnectivityWatcherService : ForegroundService() { private suspend fun manageVpn() { while (true) { when { - ((isEthernetConnected && - setting.isTunnelOnEthernetEnabled && - vpnService.getState() == Tunnel.State.DOWN)) -> + ( + ( + isEthernetConnected && + setting.isTunnelOnEthernetEnabled && + vpnService.getState() == Tunnel.State.DOWN + ) + ) -> ServiceManager.startVpnService(this, tunnelConfig) - (!isEthernetConnected && - setting.isTunnelOnMobileDataEnabled && - !isWifiConnected && - isMobileDataConnected && - vpnService.getState() == Tunnel.State.DOWN) -> + ( + !isEthernetConnected && + setting.isTunnelOnMobileDataEnabled && + !isWifiConnected && + isMobileDataConnected && + vpnService.getState() == Tunnel.State.DOWN + ) -> ServiceManager.startVpnService(this, tunnelConfig) - (!isEthernetConnected && - !setting.isTunnelOnMobileDataEnabled && - !isWifiConnected && - vpnService.getState() == Tunnel.State.UP) -> + ( + !isEthernetConnected && + !setting.isTunnelOnMobileDataEnabled && + !isWifiConnected && + vpnService.getState() == Tunnel.State.UP + ) -> ServiceManager.stopVpnService(this) - (!isEthernetConnected && isWifiConnected && - !setting.trustedNetworkSSIDs.contains(currentNetworkSSID) && - setting.isTunnelOnWifiEnabled && - (vpnService.getState() != Tunnel.State.UP)) -> + ( + !isEthernetConnected && isWifiConnected && + !setting.trustedNetworkSSIDs.contains(currentNetworkSSID) && + setting.isTunnelOnWifiEnabled && + (vpnService.getState() != Tunnel.State.UP) + ) -> ServiceManager.startVpnService(this, tunnelConfig) - (!isEthernetConnected && (isWifiConnected && - setting.trustedNetworkSSIDs.contains(currentNetworkSSID)) && - (vpnService.getState() == Tunnel.State.UP)) -> + ( + !isEthernetConnected && ( + isWifiConnected && + setting.trustedNetworkSSIDs.contains(currentNetworkSSID) + ) && + (vpnService.getState() == Tunnel.State.UP) + ) -> ServiceManager.stopVpnService(this) - (!isEthernetConnected && (isWifiConnected && - !setting.isTunnelOnWifiEnabled && - (vpnService.getState() == Tunnel.State.UP))) -> + ( + !isEthernetConnected && ( + isWifiConnected && + !setting.isTunnelOnWifiEnabled && + (vpnService.getState() == Tunnel.State.UP) + ) + ) -> ServiceManager.stopVpnService(this) - (!isEthernetConnected && !isWifiConnected && - !isMobileDataConnected && - (vpnService.getState() == Tunnel.State.UP)) -> + ( + !isEthernetConnected && !isWifiConnected && + !isMobileDataConnected && + (vpnService.getState() == Tunnel.State.UP) + ) -> ServiceManager.stopVpnService(this) + else -> { - Timber.d("Unknown case") + // Do nothing } } delay(Constants.VPN_CONNECTIVITY_CHECK_INTERVAL) } } -} \ No newline at end of file +} 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 6a1cd30..a54fc6d 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 @@ -3,7 +3,9 @@ package com.zaneschepke.wireguardautotunnel.service.foreground import android.app.PendingIntent import android.content.Intent import android.os.Bundle +import androidx.core.app.ServiceCompat import androidx.lifecycle.lifecycleScope +import com.zaneschepke.wireguardautotunnel.Constants import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa @@ -12,29 +14,28 @@ import com.zaneschepke.wireguardautotunnel.service.notification.NotificationServ import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject @AndroidEntryPoint class WireGuardTunnelService : ForegroundService() { - private val foregroundId = 123 @Inject - lateinit var vpnService : VpnService + lateinit var vpnService: VpnService @Inject lateinit var settingsRepo: SettingsDoa @Inject - lateinit var notificationService : NotificationService + lateinit var notificationService: NotificationService - private lateinit var job : Job + private lateinit var job: Job - private var tunnelName : String = "" + private var tunnelName: String = "" override fun onCreate() { super.onCreate() @@ -43,69 +44,76 @@ class WireGuardTunnelService : ForegroundService() { } } - override fun startService(extras : Bundle?) { + override fun startService(extras: Bundle?) { super.startService(extras) launchVpnStartingNotification() val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key)) cancelJob() - job = lifecycleScope.launch(Dispatchers.IO) { - launch { - if(tunnelConfigString != null) { - try { - val tunnelConfig = TunnelConfig.from(tunnelConfigString) - tunnelName = tunnelConfig.name - vpnService.startTunnel(tunnelConfig) - } catch (e : Exception) { - Timber.e("Problem starting tunnel: ${e.message}") - stopService(extras) - } - } else { - Timber.d("Tunnel config null, starting default tunnel") - val settings = settingsRepo.getAll() - if(settings.isNotEmpty()) { - val setting = settings[0] - if(setting.defaultTunnel != null && setting.isAlwaysOnVpnEnabled) { - val tunnelConfig = TunnelConfig.from(setting.defaultTunnel!!) + job = + lifecycleScope.launch(Dispatchers.IO) { + launch { + if (tunnelConfigString != null) { + try { + val tunnelConfig = TunnelConfig.from(tunnelConfigString) tunnelName = tunnelConfig.name vpnService.startTunnel(tunnelConfig) + } catch (e: Exception) { + Timber.e("Problem starting tunnel: ${e.message}") + stopService(extras) + } + } else { + Timber.d("Tunnel config null, starting default tunnel") + val settings = settingsRepo.getAll() + if (settings.isNotEmpty()) { + val setting = settings[0] + if (setting.defaultTunnel != null && setting.isAlwaysOnVpnEnabled) { + val tunnelConfig = TunnelConfig.from(setting.defaultTunnel!!) + tunnelName = tunnelConfig.name + vpnService.startTunnel(tunnelConfig) + } } } } - } - launch { - var didShowConnected = false - var didShowFailedHandshakeNotification = false - vpnService.handshakeStatus.collect { - when(it) { - HandshakeStatus.NOT_STARTED -> { - } - HandshakeStatus.NEVER_CONNECTED -> { - if(!didShowFailedHandshakeNotification) { - launchVpnConnectionFailedNotification(getString(R.string.initial_connection_failure_message)) - didShowFailedHandshakeNotification = true - didShowConnected = false + launch { + var didShowConnected = false + var didShowFailedHandshakeNotification = false + vpnService.handshakeStatus.collect { + when (it) { + HandshakeStatus.NOT_STARTED -> { } - } - HandshakeStatus.HEALTHY -> { - if(!didShowConnected) { - launchVpnConnectedNotification() - didShowConnected = true + HandshakeStatus.NEVER_CONNECTED -> { + if (!didShowFailedHandshakeNotification) { + launchVpnConnectionFailedNotification( + getString(R.string.initial_connection_failure_message) + ) + didShowFailedHandshakeNotification = true + didShowConnected = false + } } - } - HandshakeStatus.UNHEALTHY -> { - if(!didShowFailedHandshakeNotification) { - launchVpnConnectionFailedNotification(getString(R.string.lost_connection_failure_message)) - didShowFailedHandshakeNotification = true - didShowConnected = false + + HandshakeStatus.HEALTHY -> { + if (!didShowConnected) { + launchVpnConnectedNotification() + didShowConnected = true + } + } + HandshakeStatus.STALE -> {} + HandshakeStatus.UNHEALTHY -> { + if (!didShowFailedHandshakeNotification) { + launchVpnConnectionFailedNotification( + getString(R.string.lost_connection_failure_message) + ) + didShowFailedHandshakeNotification = true + didShowConnected = false + } } } } } } - } } - override fun stopService(extras : Bundle?) { + override fun stopService(extras: Bundle?) { super.stopService(extras) lifecycleScope.launch(Dispatchers.IO) { vpnService.stopTunnel() @@ -115,51 +123,68 @@ class WireGuardTunnelService : ForegroundService() { } private fun launchVpnConnectedNotification() { - val notification = notificationService.createNotification( - channelId = getString(R.string.vpn_channel_id), - channelName = getString(R.string.vpn_channel_name), - title = getString(R.string.tunnel_start_title), - onGoing = false, - vibration = false, - showTimestamp = true, - description = "${getString(R.string.tunnel_start_text)} $tunnelName" + val notification = + notificationService.createNotification( + channelId = getString(R.string.vpn_channel_id), + channelName = getString(R.string.vpn_channel_name), + title = getString(R.string.tunnel_start_title), + onGoing = false, + vibration = false, + showTimestamp = true, + description = "${getString(R.string.tunnel_start_text)} $tunnelName" + ) + ServiceCompat.startForeground( + this, + foregroundId, + notification, + Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID ) - super.startForeground(foregroundId, notification) } private fun launchVpnStartingNotification() { - val notification = notificationService.createNotification( - channelId = getString(R.string.vpn_channel_id), - channelName = getString(R.string.vpn_channel_name), - title = getString(R.string.vpn_starting), - onGoing = false, - vibration = false, - showTimestamp = true, - description = getString(R.string.attempt_connection) + val notification = + notificationService.createNotification( + channelId = getString(R.string.vpn_channel_id), + channelName = getString(R.string.vpn_channel_name), + title = getString(R.string.vpn_starting), + onGoing = false, + vibration = false, + showTimestamp = true, + description = getString(R.string.attempt_connection) + ) + ServiceCompat.startForeground( + this, + foregroundId, + notification, + Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID ) - super.startForeground(foregroundId, notification) } - 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 - ) + 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 + ) super.startForeground(foregroundId, notification) } - private fun cancelJob() { - if(this::job.isInitialized) { + if (this::job.isInitialized) { job.cancel() } } -} \ No newline at end of file +} 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 b9e5e8d..4f5e416 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 @@ -14,69 +14,82 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.map - -abstract class BaseNetworkService>(val context: Context, networkCapability : Int) : NetworkService { +abstract class BaseNetworkService>( + val context: Context, + networkCapability: Int +) : NetworkService { private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 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 onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities + ) { + trySend( + NetworkStatus.CapabilitiesChanged( + network, + networkCapabilities + ) + ) + } + } } - override fun onLost(network: Network) { - trySend(NetworkStatus.Unavailable(network)) - } + else -> { + object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + trySend(NetworkStatus.Available(network)) + } - override fun onCapabilitiesChanged( - network: Network, - networkCapabilities: NetworkCapabilities - ) { - trySend(NetworkStatus.CapabilitiesChanged(network, networkCapabilities)) + 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) - else -> { - object : ConnectivityManager.NetworkCallback() { - - override fun onAvailable(network: Network) { - trySend(NetworkStatus.Available(network)) - } - - override fun onLost(network: Network) { - trySend(NetworkStatus.Unavailable(network)) - } - - override fun onCapabilitiesChanged( - network: Network, - networkCapabilities: NetworkCapabilities - ) { - trySend(NetworkStatus.CapabilitiesChanged(network, networkCapabilities)) - } - } + awaitClose { + connectivityManager.unregisterNetworkCallback(networkStatusCallback) } } - val request = NetworkRequest.Builder() - .addTransportType(networkCapability) - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) - .build() - connectivityManager.registerNetworkCallback(request, networkStatusCallback) - - awaitClose { - connectivityManager.unregisterNetworkCallback(networkStatusCallback) - } - } - override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? { var ssid: String? = getWifiNameFromCapabilities(networkCapabilities) @@ -89,7 +102,6 @@ abstract class BaseNetworkService>(val context: Contex return ssid?.trim('"') } - companion object { private fun getWifiNameFromCapabilities(networkCapabilities: NetworkCapabilities): String? { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { @@ -105,13 +117,20 @@ abstract class BaseNetworkService>(val context: Contex } inline fun Flow.map( - crossinline onUnavailable: suspend (network : Network) -> Result, - crossinline onAvailable: suspend (network : Network) -> Result, - crossinline onCapabilitiesChanged: suspend (network : Network, networkCapabilities : NetworkCapabilities) -> Result, -): Flow = map { status -> - when (status) { - is NetworkStatus.Unavailable -> onUnavailable(status.network) - is NetworkStatus.Available -> onAvailable(status.network) - is NetworkStatus.CapabilitiesChanged -> onCapabilitiesChanged(status.network, status.networkCapabilities) + 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 + ) + } } -} \ No newline at end of file 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 5450ca3..1f86836 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,6 +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) { -} \ No newline at end of file +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 df61c52..a488e3a 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,6 +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) { -} \ No newline at end of file +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 e9fc3bd..88c9416 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 @@ -5,5 +5,6 @@ import kotlinx.coroutines.flow.Flow interface NetworkService { fun getNetworkName(networkCapabilities: NetworkCapabilities): String? + 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 ab895a8..63365a0 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,7 +4,10 @@ import android.net.Network import android.net.NetworkCapabilities sealed class NetworkStatus { - class Available(val network : Network) : NetworkStatus() - class Unavailable(val network : Network) : NetworkStatus() - class CapabilitiesChanged(val network : Network, val networkCapabilities : NetworkCapabilities) : NetworkStatus() + class Available(val network: Network) : NetworkStatus() + + class Unavailable(val network: Network) : 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 bbdf0a7..ebc4797 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,6 +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) { -} \ No newline at end of file +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 29806ae..9058fea 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 @@ -12,10 +12,10 @@ interface NotificationService { action: PendingIntent? = null, actionText: String? = null, description: String, - showTimestamp : Boolean = false, + showTimestamp: Boolean = false, importance: Int = NotificationManager.IMPORTANCE_HIGH, vibration: Boolean = false, onGoing: Boolean = true, lights: Boolean = true ): Notification -} \ No newline at end of file +} 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 a982de7..38c7d20 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 @@ -12,9 +12,13 @@ import com.zaneschepke.wireguardautotunnel.ui.MainActivity import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject -class WireGuardNotification @Inject constructor(@ApplicationContext private val context: Context) : NotificationService { - - 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 override fun createNotification( channelId: String, @@ -29,18 +33,19 @@ class WireGuardNotification @Inject constructor(@ApplicationContext private val onGoing: Boolean, lights: 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, 400, 500, 400, 300, 200, 400) - it - } + 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, 400, 500, 400, 300, 200, 400) + it + } notificationManager.createNotificationChannel(channel) val pendingIntent: PendingIntent = Intent(context, MainActivity::class.java).let { notificationIntent -> @@ -58,14 +63,15 @@ class WireGuardNotification @Inject constructor(@ApplicationContext private val channelId ) return builder.let { - if(action != null && actionText != null) { - //TODO find a not deprecated way to do this + if (action != null && actionText != null) { + // TODO find a not deprecated way to do this it.addAction( Notification.Action.Builder(0, actionText, action) - .build()) - it.setAutoCancel(true) + .build() + ) + it.setAutoCancel(true) } - it.setContentTitle(title) + it.setContentTitle(title) .setContentText(description) .setContentIntent(pendingIntent) .setOngoing(onGoing) @@ -74,4 +80,4 @@ class WireGuardNotification @Inject constructor(@ApplicationContext private val .build() } } -} \ No newline at end of file +} 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 76c23e2..ac48f3c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt @@ -12,24 +12,23 @@ import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService import com.zaneschepke.wireguardautotunnel.util.WgTunnelException import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject @AndroidEntryPoint class ShortcutsActivity : ComponentActivity() { + @Inject + lateinit var settingsRepo: SettingsDoa @Inject - lateinit var settingsRepo : SettingsDoa + lateinit var tunnelConfigRepo: TunnelConfigDao - @Inject - lateinit var tunnelConfigRepo : TunnelConfigDao - - private fun attemptWatcherServiceToggle(tunnelConfig : String) { + private fun attemptWatcherServiceToggle(tunnelConfig: String) { lifecycleScope.launch(Dispatchers.Main) { val settings = getSettings() - if(settings.isAutoTunnelEnabled) { + if (settings.isAutoTunnelEnabled) { ServiceManager.toggleWatcherServiceForeground(this@ShortcutsActivity, tunnelConfig) } } @@ -37,29 +36,36 @@ class ShortcutsActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if(intent.getStringExtra(CLASS_NAME_EXTRA_KEY) - .equals(WireGuardTunnelService::class.java.simpleName)) { + if (intent.getStringExtra(CLASS_NAME_EXTRA_KEY) + .equals(WireGuardTunnelService::class.java.simpleName) + ) { lifecycleScope.launch(Dispatchers.Main) { val settings = getSettings() - if(settings.isShortcutsEnabled) { + if (settings.isShortcutsEnabled) { try { val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY) - val tunnelConfig = if(tunnelName != null) { - tunnelConfigRepo.getAll().firstOrNull { it.name == tunnelName } - } else { - if(settings.defaultTunnel == null) { - tunnelConfigRepo.getAll().first() + val tunnelConfig = + if (tunnelName != null) { + tunnelConfigRepo.getAll().firstOrNull { it.name == tunnelName } } else { - TunnelConfig.from(settings.defaultTunnel!!) + if (settings.defaultTunnel == null) { + tunnelConfigRepo.getAll().first() + } else { + TunnelConfig.from(settings.defaultTunnel!!) + } } - } tunnelConfig ?: return@launch attemptWatcherServiceToggle(tunnelConfig.toString()) - when(intent.action){ - Action.STOP.name -> ServiceManager.stopVpnService(this@ShortcutsActivity) - Action.START.name -> ServiceManager.startVpnService(this@ShortcutsActivity, tunnelConfig.toString()) + when (intent.action) { + Action.STOP.name -> ServiceManager.stopVpnService( + this@ShortcutsActivity + ) + Action.START.name -> ServiceManager.startVpnService( + this@ShortcutsActivity, + tunnelConfig.toString() + ) } - } catch (e : Exception) { + } catch (e: Exception) { Timber.e(e.message) } } @@ -68,7 +74,7 @@ class ShortcutsActivity : ComponentActivity() { finish() } - private suspend fun getSettings() : Settings { + private suspend fun getSettings(): Settings { val settings = settingsRepo.getAll() return if (settings.isNotEmpty()) { settings.first() @@ -76,8 +82,9 @@ class ShortcutsActivity : ComponentActivity() { throw WgTunnelException("Settings empty") } } + companion object { const val TUNNEL_NAME_EXTRA_KEY = "tunnelName" const val CLASS_NAME_EXTRA_KEY = "className" } -} \ No newline at end of file +} 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 4f27cf9..dc4b3f7 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 @@ -11,34 +11,34 @@ import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject @AndroidEntryPoint class TunnelControlTile : TileService() { + @Inject + lateinit var settingsRepo: SettingsDoa @Inject - lateinit var settingsRepo : SettingsDoa + lateinit var configRepo: TunnelConfigDao @Inject - lateinit var configRepo : TunnelConfigDao - - @Inject - lateinit var vpnService : VpnService + lateinit var vpnService: VpnService private val scope = CoroutineScope(Dispatchers.Main) - private lateinit var job : Job + private lateinit var job: Job override fun onStartListening() { - job = scope.launch { - updateTileState() - } + job = + scope.launch { + updateTileState() + } super.onStartListening() } @@ -58,15 +58,18 @@ class TunnelControlTile : TileService() { scope.launch { try { val tunnel = determineTileTunnel() - if(tunnel != null) { + if (tunnel != null) { attemptWatcherServiceToggle(tunnel.toString()) - if(vpnService.getState() == Tunnel.State.UP) { + if (vpnService.getState() == Tunnel.State.UP) { ServiceManager.stopVpnService(this@TunnelControlTile) } else { - ServiceManager.startVpnServiceForeground(this@TunnelControlTile, tunnel.toString()) + ServiceManager.startVpnServiceForeground( + this@TunnelControlTile, + tunnel.toString() + ) } } - } catch (e : Exception) { + } catch (e: Exception) { Timber.e(e.message) } finally { cancel() @@ -75,34 +78,38 @@ class TunnelControlTile : TileService() { } } - private suspend fun determineTileTunnel() : TunnelConfig? { - var tunnelConfig : TunnelConfig? = null + private suspend fun determineTileTunnel(): TunnelConfig? { + var tunnelConfig: TunnelConfig? = null val settings = settingsRepo.getAll() if (settings.isNotEmpty()) { val setting = settings.first() - tunnelConfig = if (setting.defaultTunnel != null) { - TunnelConfig.from(setting.defaultTunnel!!) - } else { - val configs = configRepo.getAll() - val config = if(configs.isNotEmpty()) { - configs.first() + tunnelConfig = + if (setting.defaultTunnel != null) { + TunnelConfig.from(setting.defaultTunnel!!) } else { - null + val configs = configRepo.getAll() + val config = + if (configs.isNotEmpty()) { + configs.first() + } else { + null + } + config } - config - } } return tunnelConfig } - - private fun attemptWatcherServiceToggle(tunnelConfig : String) { + private fun attemptWatcherServiceToggle(tunnelConfig: String) { scope.launch { val settings = settingsRepo.getAll() if (settings.isNotEmpty()) { val setting = settings.first() - if(setting.isAutoTunnelEnabled) { - ServiceManager.toggleWatcherServiceForeground(this@TunnelControlTile, tunnelConfig) + if (setting.isAutoTunnelEnabled) { + ServiceManager.toggleWatcherServiceForeground( + this@TunnelControlTile, + tunnelConfig + ) } } } @@ -111,27 +118,31 @@ class TunnelControlTile : TileService() { private suspend fun updateTileState() { vpnService.state.collect { try { - when(it) { + when (it) { Tunnel.State.UP -> { qsTile.state = Tile.STATE_ACTIVE } + Tunnel.State.DOWN -> { qsTile.state = Tile.STATE_INACTIVE } + else -> { qsTile.state = Tile.STATE_UNAVAILABLE } } val config = determineTileTunnel() - setTileDescription(config?.name ?: this.resources.getString(R.string.no_tunnel_available)) + setTileDescription( + config?.name ?: this.resources.getString(R.string.no_tunnel_available) + ) qsTile.updateTile() - } catch (e : Exception) { + } catch (e: Exception) { Timber.e("Unable to update tile state") } } } - private fun setTileDescription(description : String) { + private fun setTileDescription(description: String) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { qsTile.subtitle = description } @@ -141,8 +152,8 @@ class TunnelControlTile : TileService() { } private fun cancelJob() { - if(this::job.isInitialized) { + if (this::job.isInitialized) { job.cancel() } } -} \ No newline at end of file +} 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 c6bbf35..a1b59c4 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 @@ -2,13 +2,16 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel enum class HandshakeStatus { HEALTHY, + STALE, UNHEALTHY, NEVER_CONNECTED, - NOT_STARTED; + NOT_STARTED + ; companion object { - private const val WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC = 120 - const val UNHEALTHY_TIME_LIMIT_SEC = WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC + 60 + 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 } -} \ No newline at end of file +} 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 1557c20..600fb2c 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 @@ -7,12 +7,15 @@ import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig import kotlinx.coroutines.flow.SharedFlow interface VpnService : Tunnel { - suspend fun startTunnel(tunnelConfig : TunnelConfig) : Tunnel.State + suspend fun startTunnel(tunnelConfig: TunnelConfig): Tunnel.State + suspend fun stopTunnel() - val state : SharedFlow - val tunnelName : SharedFlow - val statistics : SharedFlow - val lastHandshake : SharedFlow> - val handshakeStatus : SharedFlow - fun getState() : Tunnel.State -} \ No newline at end of file + + val state: SharedFlow + val tunnelName: SharedFlow + val statistics: SharedFlow + val lastHandshake: SharedFlow> + val handshakeStatus: SharedFlow + + fun getState(): Tunnel.State +} 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 d18ecf2..ac05c49 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 @@ -4,10 +4,15 @@ import com.wireguard.android.backend.Backend import com.wireguard.android.backend.BackendException import com.wireguard.android.backend.Statistics import com.wireguard.android.backend.Tunnel +import com.wireguard.config.Config import com.wireguard.crypto.Key import com.zaneschepke.wireguardautotunnel.Constants +import com.zaneschepke.wireguardautotunnel.module.Kernel +import com.zaneschepke.wireguardautotunnel.module.Userspace +import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.util.NumberUtils +import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -20,20 +25,28 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject - - -class WireGuardTunnel @Inject constructor(private val backend : Backend) : VpnService { +class WireGuardTunnel +@Inject +constructor( + @Userspace private val userspaceBackend: Backend, + @Kernel private val kernelBackend: Backend, + private val settingsRepo: SettingsDoa +) : VpnService { private val _tunnelName = MutableStateFlow("") override val tunnelName get() = _tunnelName.asStateFlow() - private val _state = MutableSharedFlow( - onBufferOverflow = BufferOverflow.DROP_OLDEST, - replay = 1) + private val _state = + MutableSharedFlow( + onBufferOverflow = BufferOverflow.DROP_OLDEST, + replay = 1 + ) - private val _handshakeStatus = MutableSharedFlow(replay = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST) + private val _handshakeStatus = + MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) override val state get() = _state.asSharedFlow() private val _statistics = MutableSharedFlow(replay = 1) @@ -47,30 +60,56 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend) : VpnSe private val scope = CoroutineScope(Dispatchers.IO) - private lateinit var statsJob : Job + private lateinit var statsJob: Job + private var config: Config? = null - override suspend fun startTunnel(tunnelConfig: TunnelConfig) : Tunnel.State{ + private var backend: Backend = userspaceBackend + + private var backendIsUserspace = true + + init { + scope.launch { + settingsRepo.getAllFlow().collect { + val settings = it.first() + if (settings.isKernelEnabled && backendIsUserspace) { + Timber.d("Setting kernel backend") + backend = kernelBackend + backendIsUserspace = false + } else if (!settings.isKernelEnabled && !backendIsUserspace) { + Timber.d("Setting userspace backend") + backend = userspaceBackend + backendIsUserspace = true + } + } + } + } + + override suspend fun startTunnel(tunnelConfig: TunnelConfig): Tunnel.State { return try { stopTunnelOnConfigChange(tunnelConfig) emitTunnelName(tunnelConfig.name) - val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick) - val state = backend.setState( - this, Tunnel.State.UP, config) + config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick) + val state = + backend.setState( + this, + Tunnel.State.UP, + config + ) _state.emit(state) state - } catch (e : Exception) { + } catch (e: Exception) { Timber.e("Failed to start tunnel with error: ${e.message}") Tunnel.State.DOWN } } - private suspend fun emitTunnelName(name : String) { + private suspend fun emitTunnelName(name: String) { _tunnelName.emit(name) } private suspend fun stopTunnelOnConfigChange(tunnelConfig: TunnelConfig) { - if(getState() == Tunnel.State.UP && _tunnelName.value != tunnelConfig.name) { + if (getState() == Tunnel.State.UP && _tunnelName.value != tunnelConfig.name) { stopTunnel() } } @@ -81,11 +120,11 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend) : VpnSe override suspend fun stopTunnel() { try { - if(getState() == Tunnel.State.UP) { + if (getState() == Tunnel.State.UP) { val state = backend.setState(this, Tunnel.State.DOWN, null) _state.emit(state) } - } catch (e : BackendException) { + } catch (e: BackendException) { Timber.e("Failed to stop tunnel with error: ${e.message}") } } @@ -94,49 +133,55 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend) : VpnSe return backend.getState(this) } - override fun onStateChange(state : Tunnel.State) { + override fun onStateChange(state: Tunnel.State) { val tunnel = this _state.tryEmit(state) - if(state == Tunnel.State.UP) { - statsJob = scope.launch { - val handshakeMap = HashMap() - var neverHadHandshakeCounter = 0 - while (true) { - val statistics = backend.getStatistics(tunnel) - _statistics.emit(statistics) - statistics.peers().forEach { - val handshakeEpoch = statistics.peer(it)?.latestHandshakeEpochMillis ?: 0L - handshakeMap[it] = handshakeEpoch - if(handshakeEpoch == 0L) { - if(neverHadHandshakeCounter >= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) { - _handshakeStatus.emit(HandshakeStatus.NEVER_CONNECTED) - } else { - _handshakeStatus.emit(HandshakeStatus.NOT_STARTED) + if (state == Tunnel.State.UP) { + statsJob = + scope.launch { + val handshakeMap = HashMap() + var neverHadHandshakeCounter = 0 + while (true) { + val statistics = backend.getStatistics(tunnel) + _statistics.emit(statistics) + statistics.peers().forEach { key -> + val handshakeEpoch = + statistics.peer(key)?.latestHandshakeEpochMillis ?: 0L + handshakeMap[key] = handshakeEpoch + if (handshakeEpoch == 0L) { + if (neverHadHandshakeCounter >= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) { + _handshakeStatus.emit(HandshakeStatus.NEVER_CONNECTED) + } else { + _handshakeStatus.emit(HandshakeStatus.NOT_STARTED) + } + if (neverHadHandshakeCounter <= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) { + neverHadHandshakeCounter += (1 * Constants.VPN_STATISTIC_CHECK_INTERVAL / 1000).toInt() + } + return@forEach } - if(neverHadHandshakeCounter <= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) { - neverHadHandshakeCounter += (1 * Constants.VPN_STATISTIC_CHECK_INTERVAL/1000).toInt() + // TODO one day make each peer have their own dedicated status + val lastHandshake = NumberUtils.getSecondsBetweenTimestampAndNow( + handshakeEpoch + ) + if (lastHandshake != null) { + if (lastHandshake >= HandshakeStatus.STALE_TIME_LIMIT_SEC) { + _handshakeStatus.emit(HandshakeStatus.STALE) + } else { + _handshakeStatus.emit(HandshakeStatus.HEALTHY) + } } - return@forEach - } - if((NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch) ?: 0L) >= HandshakeStatus.UNHEALTHY_TIME_LIMIT_SEC) { - _handshakeStatus.emit(HandshakeStatus.UNHEALTHY) - } else { - _handshakeStatus.emit(HandshakeStatus.HEALTHY) } + _lastHandshake.emit(handshakeMap) + delay(Constants.VPN_STATISTIC_CHECK_INTERVAL) } - _lastHandshake.emit(handshakeMap) - delay(Constants.VPN_STATISTIC_CHECK_INTERVAL) } - } } - if(state == Tunnel.State.DOWN) { - if(this::statsJob.isInitialized) { + if (state == Tunnel.State.DOWN) { + if (this::statsJob.isInitialized) { statsJob.cancel() } _handshakeStatus.tryEmit(HandshakeStatus.NOT_STARTED) _lastHandshake.tryEmit(emptyMap()) } } - - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/ActivityViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/ActivityViewModel.kt new file mode 100644 index 0000000..d583e16 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/ActivityViewModel.kt @@ -0,0 +1,8 @@ +package com.zaneschepke.wireguardautotunnel.ui + +import androidx.lifecycle.ViewModel +import javax.inject.Inject + +class ActivityViewModel @Inject constructor() : ViewModel() { + // TODO move shared logic to shared viewmodel +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/CaptureActivityPortrait.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/CaptureActivityPortrait.kt index 9972857..029ade5 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/CaptureActivityPortrait.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/CaptureActivityPortrait.kt @@ -2,4 +2,4 @@ package com.zaneschepke.wireguardautotunnel.ui import com.journeyapps.barcodescanner.CaptureActivity -class CaptureActivityPortrait : CaptureActivity() \ No newline at end of file +class CaptureActivityPortrait : CaptureActivity() 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 186f7d3..40c766a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt @@ -12,7 +12,6 @@ import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.ExitTransition -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.slideInHorizontally @@ -59,13 +58,14 @@ import timber.log.Timber @AndroidEntryPoint class MainActivity : AppCompatActivity() { - - @OptIn(ExperimentalAnimationApi::class, + @OptIn( ExperimentalPermissionsApi::class ) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { + // TODO move shared logic to shared viewmodel + // val sharedViewModel = hiltViewModel() val navController = rememberNavController() val focusRequester = remember { FocusRequester() } @@ -84,68 +84,86 @@ class MainActivity : AppCompatActivity() { } var vpnIntent by remember { mutableStateOf(GoBackend.VpnService.prepare(this)) } - val vpnActivityResultState = rememberLauncherForActivityResult( - ActivityResultContracts.StartActivityForResult(), - onResult = { - val accepted = (it.resultCode == RESULT_OK) - if (accepted) { - vpnIntent = null + val vpnActivityResultState = + rememberLauncherForActivityResult( + ActivityResultContracts.StartActivityForResult(), + onResult = { + val accepted = (it.resultCode == RESULT_OK) + if (accepted) { + vpnIntent = null + } } - }) + ) LaunchedEffect(vpnIntent) { if (vpnIntent != null) { vpnActivityResultState.launch(vpnIntent) - } else requestNotificationPermission() + } else { + requestNotificationPermission() + } } - fun showSnackBarMessage(message : String) { + fun showSnackBarMessage(message: String) { lifecycleScope.launch(Dispatchers.Main) { - val result = snackbarHostState.showSnackbar( - message = message, - actionLabel = applicationContext.getString(R.string.okay), - duration = SnackbarDuration.Short, - ) + val result = + snackbarHostState.showSnackbar( + message = message, + actionLabel = applicationContext.getString(R.string.okay), + duration = SnackbarDuration.Short + ) when (result) { - SnackbarResult.ActionPerformed -> { snackbarHostState.currentSnackbarData?.dismiss() } - SnackbarResult.Dismissed -> { snackbarHostState.currentSnackbarData?.dismiss() } + SnackbarResult.ActionPerformed -> { + snackbarHostState.currentSnackbarData?.dismiss() + } + + SnackbarResult.Dismissed -> { + snackbarHostState.currentSnackbarData?.dismiss() + } } } } - Scaffold(snackbarHost = { + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) { snackbarData: SnackbarData -> CustomSnackBar( snackbarData.visuals.message, isRtl = false, - containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) + containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( + 2.dp + ) ) } }, - modifier = Modifier.onKeyEvent { + modifier = + Modifier.onKeyEvent { if (it.nativeKeyEvent.action == KeyEvent.ACTION_UP) { when (it.nativeKeyEvent.keyCode) { KeyEvent.KEYCODE_DPAD_UP -> { try { focusRequester.requestFocus() - } catch(e : IllegalStateException) { - Timber.e("No D-Pad focus request modifier added to element on screen") + } catch (e: IllegalStateException) { + Timber.e( + "No D-Pad focus request modifier added to element on screen" + ) } false - } else -> { - false + } + + else -> { + false } } } else { false } }, - bottomBar = if (vpnIntent == null && notificationPermissionState.status.isGranted) { + bottomBar = + if (vpnIntent == null && notificationPermissionState.status.isGranted) { { BottomNavBar(navController, Routes.navItems) } } else { {} - }, - ) - { padding -> + } + ) { padding -> if (vpnIntent != null) { PermissionRequestFailedScreen( padding = padding, @@ -162,7 +180,11 @@ class MainActivity : AppCompatActivity() { val intentSettings = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) intentSettings.data = - Uri.fromParts(Constants.URI_PACKAGE_SCHEME, this.packageName, null) + Uri.fromParts( + Constants.URI_PACKAGE_SCHEME, + this.packageName, + null + ) startActivity(intentSettings) }, message = getString(R.string.notification_permission_required), @@ -172,23 +194,36 @@ class MainActivity : AppCompatActivity() { } NavHost(navController, startDestination = Routes.Main.name) { - composable(Routes.Main.name, enterTransition = { - when (initialState.destination.route) { - Routes.Settings.name, Routes.Support.name -> - slideInHorizontally( - initialOffsetX = { -Constants.SLIDE_IN_TRANSITION_OFFSET }, - animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION) - ) + composable( + Routes.Main.name, + enterTransition = { + when (initialState.destination.route) { + Routes.Settings.name, Routes.Support.name -> + slideInHorizontally( + initialOffsetX = { + -Constants.SLIDE_IN_TRANSITION_OFFSET + }, + animationSpec = tween( + Constants.SLIDE_IN_ANIMATION_DURATION + ) + ) - else -> { - fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION)) + else -> { + fadeIn( + animationSpec = tween( + Constants.FADE_IN_ANIMATION_DURATION + ) + ) + } } + }, + exitTransition = { + ExitTransition.None } - }, exitTransition = { - ExitTransition.None - } - ) { - MainScreen(padding = padding, showSnackbarMessage = { message -> showSnackBarMessage(message) }, navController = navController) + ) { + MainScreen(padding = padding, showSnackbarMessage = { message -> + showSnackBarMessage(message) + }, navController = navController) } composable(Routes.Settings.name, enterTransition = { when (initialState.destination.route) { @@ -206,10 +241,16 @@ class MainActivity : AppCompatActivity() { } else -> { - fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION)) + fadeIn( + animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION) + ) } } - }) { SettingsScreen(padding = padding, showSnackbarMessage = { message -> showSnackBarMessage(message) }, focusRequester = focusRequester) } + }) { + SettingsScreen(padding = padding, showSnackbarMessage = { message -> + showSnackBarMessage(message) + }, focusRequester = focusRequester) + } composable(Routes.Support.name, enterTransition = { when (initialState.destination.route) { Routes.Settings.name, Routes.Main.name -> @@ -219,16 +260,26 @@ class MainActivity : AppCompatActivity() { ) else -> { - fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION)) + fadeIn( + animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION) + ) } } - }) { SupportScreen(padding = padding, focusRequester) } + }) { SupportScreen(padding = padding, focusRequester = focusRequester) } composable("${Routes.Config.name}/{id}", enterTransition = { fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION)) - }) { it -> + }) { val id = it.arguments?.getString("id") - if(!id.isNullOrBlank()) { - ConfigScreen(navController = navController, id = id, showSnackbarMessage = { message -> showSnackBarMessage(message) }, focusRequester = focusRequester)} + if (!id.isNullOrBlank()) { + ConfigScreen( + navController = navController, + id = id, + showSnackbarMessage = { message -> + showSnackBarMessage(message) + }, + focusRequester = focusRequester + ) + } } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Routes.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Routes.kt index a78fedd..cc4d3df 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Routes.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Routes.kt @@ -10,26 +10,27 @@ enum class Routes { Main, Settings, Support, - Config; - + Config + ; companion object { - val navItems = listOf( - BottomNavItem( - name = "Tunnels", - route = Main.name, - icon = Icons.Rounded.Home, - ), - BottomNavItem( - name = "Settings", - route = Settings.name, - icon = Icons.Rounded.Settings, - ), - BottomNavItem( - name = "Support", - route = Support.name, - icon = Icons.Rounded.QuestionMark, + val navItems = + listOf( + BottomNavItem( + name = "Tunnels", + route = Main.name, + icon = Icons.Rounded.Home + ), + BottomNavItem( + name = "Settings", + route = Settings.name, + icon = Icons.Rounded.Settings + ), + BottomNavItem( + name = "Support", + route = Support.name, + icon = Icons.Rounded.QuestionMark + ) ) - ) } -} \ No newline at end of file +} 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 3378aef..023659c 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 @@ -1,8 +1,10 @@ package com.zaneschepke.wireguardautotunnel.ui.common import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -11,23 +13,31 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import com.zaneschepke.wireguardautotunnel.R @Composable -fun ClickableIconButton(onIconClick : () -> Unit, text : String, icon : ImageVector, enabled : Boolean) { - TextButton(onClick = {}, +fun ClickableIconButton( + onIconClick: () -> Unit, + text: String, + icon: ImageVector, + enabled: Boolean +) { + TextButton( + onClick = {}, enabled = enabled ) { - Text(text) + Text(text, Modifier.weight(1f, false)) Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing)) Icon( imageVector = icon, contentDescription = stringResource(R.string.delete), - modifier = Modifier.size(ButtonDefaults.IconSize).clickable { - if(enabled) { + modifier = + Modifier.size(ButtonDefaults.IconSize).weight(1f, false).clickable { + if (enabled) { onIconClick() } } ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/PermissionRequestFailedScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/PermissionRequestFailedScreen.kt index 090c374..686c73a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/PermissionRequestFailedScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/PermissionRequestFailedScreen.kt @@ -16,13 +16,21 @@ import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch @Composable -fun PermissionRequestFailedScreen(padding : PaddingValues, onRequestAgain : () -> Unit, message : String, buttonText : String ) { +fun PermissionRequestFailedScreen( + padding: PaddingValues, + onRequestAgain: () -> Unit, + message: String, + buttonText: String +) { val scope = rememberCoroutineScope() - Column(horizontalAlignment = Alignment.CenterHorizontally, + Column( + horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, - modifier = Modifier + modifier = + Modifier .fillMaxSize() - .padding(padding)) { + .padding(padding) + ) { Text(message, textAlign = TextAlign.Center, modifier = Modifier.padding(15.dp)) Button(onClick = { scope.launch { @@ -32,4 +40,4 @@ fun PermissionRequestFailedScreen(padding : PaddingValues, onRequestAgain : () - Text(buttonText) } } -} \ No newline at end of file +} 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 709cbda..02f79d1 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 @@ -23,12 +23,18 @@ import com.zaneschepke.wireguardautotunnel.util.NumberUtils @OptIn(ExperimentalFoundationApi::class) @Composable -fun RowListItem(icon : @Composable () -> Unit, text : String, onHold : () -> Unit, - onClick: () -> Unit, rowButton : @Composable () -> Unit, - expanded : Boolean, statistics: Statistics? - ) { +fun RowListItem( + icon: @Composable () -> Unit, + text: String, + onHold: () -> Unit, + onClick: () -> Unit, + rowButton: @Composable () -> Unit, + expanded: Boolean, + statistics: Statistics? +) { Box( - modifier = Modifier + modifier = + Modifier .animateContentSize() .clip(RoundedCornerShape(30.dp)) .combinedClickable( @@ -42,22 +48,27 @@ fun RowListItem(icon : @Composable () -> Unit, text : String, onHold : () -> Uni ) { Column { Row( - modifier = Modifier + modifier = + Modifier .fillMaxWidth() .padding(horizontal = 14.dp, vertical = 5.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { - Row(verticalAlignment = Alignment.CenterVertically,) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(.60f) + ) { icon() Text(text) } rowButton() } - if(expanded) { + if (expanded) { statistics?.peers()?.forEach { Row( - modifier = Modifier + modifier = + Modifier .fillMaxWidth() .padding(end = 10.dp, bottom = 10.dp, start = 10.dp), verticalAlignment = Alignment.CenterVertically, @@ -66,9 +77,11 @@ fun RowListItem(icon : @Composable () -> Unit, text : String, onHold : () -> Uni val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis val peerTx = statistics.peer(it)!!.txBytes val peerRx = statistics.peer(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 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 @@ -81,4 +94,4 @@ fun RowListItem(icon : @Composable () -> Unit, text : String, onHold : () -> Uni } } } -} \ No newline at end of file +} 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 7deea5a..4016f31 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 @@ -25,9 +25,7 @@ import androidx.compose.ui.text.input.KeyboardType import com.zaneschepke.wireguardautotunnel.R @Composable -fun SearchBar( - onQuery : (queryString : String) -> Unit -) { +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) } @@ -64,17 +62,19 @@ fun SearchBar( } }, maxLines = 1, - colors = TextFieldDefaults.colors( + colors = + TextFieldDefaults.colors( focusedContainerColor = Color.Transparent, unfocusedContainerColor = Color.Transparent, - disabledContainerColor = Color.Transparent, + disabledContainerColor = Color.Transparent ), placeholder = { Text(text = stringResource(R.string.hint_search_packages)) }, textStyle = MaterialTheme.typography.bodySmall, singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), - modifier = Modifier + modifier = + Modifier .fillMaxWidth() .background(color = MaterialTheme.colorScheme.background, shape = RectangleShape) ) -} \ No newline at end of file +} 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 a65eb99..2bb3dfa 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 @@ -10,9 +10,15 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization @Composable -fun - ConfigurationTextBox(value : String, hint : String, onValueChange : (String) -> Unit, keyboardActions : KeyboardActions, label : String, modifier: Modifier) { - OutlinedTextField( +fun ConfigurationTextBox( + value: String, + hint: String, + onValueChange: (String) -> Unit, + keyboardActions: KeyboardActions, + label: String, + modifier: Modifier +) { + OutlinedTextField( modifier = modifier, value = value, singleLine = true, @@ -24,10 +30,11 @@ fun placeholder = { Text(hint) }, - keyboardOptions = KeyboardOptions( + keyboardOptions = + KeyboardOptions( capitalization = KeyboardCapitalization.None, imeAction = ImeAction.Done ), - keyboardActions = keyboardActions, + keyboardActions = keyboardActions ) -} \ No newline at end of file +} 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 99c2612..a4ae21f 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 @@ -12,10 +12,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp @Composable -fun ConfigurationToggle(label : String, enabled : Boolean, checked : Boolean, padding : Dp, - onCheckChanged : () -> Unit, modifier : Modifier = Modifier) { +fun ConfigurationToggle( + label: String, + enabled: Boolean, + checked: Boolean, + padding: Dp, + onCheckChanged: () -> Unit, + modifier: Modifier = Modifier +) { Row( - modifier = Modifier + modifier = + Modifier .fillMaxWidth() .padding(padding), verticalAlignment = Alignment.CenterVertically, @@ -31,4 +38,4 @@ fun ConfigurationToggle(label : String, enabled : Boolean, checked : Boolean, pa } ) } -} \ No newline at end of file +} 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 d99c3a2..1c3e819 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt @@ -11,12 +11,14 @@ import androidx.navigation.NavController import androidx.navigation.compose.currentBackStackEntryAsState @Composable -fun BottomNavBar(navController : NavController, bottomNavItems : List) { - +fun BottomNavBar( + navController: NavController, + bottomNavItems: List +) { val backStackEntry = navController.currentBackStackEntryAsState() NavigationBar( - containerColor = MaterialTheme.colorScheme.background, + containerColor = MaterialTheme.colorScheme.background ) { bottomNavItems.forEach { item -> val selected = item.route == backStackEntry.value?.destination?.route @@ -27,16 +29,16 @@ fun BottomNavBar(navController : NavController, bottomNavItems : List Unit, onFailure : () -> Unit, onError : (String) -> Unit) { +fun AuthorizationPrompt( + onSuccess: () -> Unit, + onFailure: () -> Unit, + onError: (String) -> Unit +) { val context = LocalContext.current val biometricManager = BiometricManager.from(context) val bio = biometricManager.canAuthenticate(BIOMETRIC_WEAK or DEVICE_CREDENTIAL) - val isBiometricAvailable = remember { - when(bio){ - BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> { - onError("Biometrics not available") - false + val isBiometricAvailable = + remember { + when (bio) { + BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> { + onError("Biometrics not available") + false + } + + BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> { + onError("Biometrics not created") + false + } + + BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> { + onError("Biometric hardware not found") + false + } + + BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> { + onError("Biometric security update required") + false + } + + BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> { + onError("Biometrics not supported") + false + } + + BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> { + onError("Biometrics status unknown") + false + } + + BiometricManager.BIOMETRIC_SUCCESS -> true + else -> false } - BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> { - onError("Biometrics not created") - false - } - BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> { - onError("Biometric hardware not found") - false - } - BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> { - onError("Biometric security update required") - false - } - BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> { - onError("Biometrics not supported") - false - } - BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> { - onError("Biometrics status unknown") - false - } - BiometricManager.BIOMETRIC_SUCCESS -> true - else -> false } - } - if(isBiometricAvailable) { + 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() + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + onFailure() + } } - } - ) + ) biometricPrompt.authenticate(promptInfo) } -} \ No newline at end of file +} 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 4732f7d..3e40e99 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 @@ -34,11 +34,14 @@ fun CustomSnackBar( containerColor: Color = MaterialTheme.colorScheme.surface ) { val context = LocalContext.current - Snackbar(containerColor = containerColor, - modifier = Modifier.fillMaxWidth( - if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) 1/3f else 2/3f).padding(bottom = 100.dp), + Snackbar( + containerColor = containerColor, + modifier = + Modifier.fillMaxWidth( + if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) 1 / 3f else 2 / 3f + ).padding(bottom = 100.dp), shape = RoundedCornerShape(16.dp) - ) { + ) { CompositionLocalProvider( LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr @@ -58,4 +61,4 @@ fun CustomSnackBar( } } } -} \ No newline at end of file +} 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 721cf77..07d543f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/text/SectionTitle.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/text/SectionTitle.kt @@ -12,11 +12,14 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @Composable -fun SectionTitle(title : String, padding : Dp) { +fun SectionTitle( + title: String, + padding: Dp +) { Text( title, textAlign = TextAlign.Center, style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.ExtraBold), modifier = Modifier.padding(padding, bottom = 5.dp, top = 5.dp) ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/InterfaceProxy.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/InterfaceProxy.kt index d5b92f9..8a44324 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/InterfaceProxy.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/InterfaceProxy.kt @@ -3,23 +3,28 @@ package com.zaneschepke.wireguardautotunnel.ui.models import com.wireguard.config.Interface data class InterfaceProxy( - var privateKey : String = "", - var publicKey : String = "", - var addresses : String = "", - var dnsServers : String = "", - var listenPort : String = "", - var mtu : String = "", -){ + var privateKey: String = "", + var publicKey: String = "", + var addresses: String = "", + var dnsServers: String = "", + var listenPort: String = "", + var mtu: String = "" +) { companion object { - fun from(i : Interface) : InterfaceProxy { + 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 "" + listenPort = if (i.listenPort.isPresent) { + i.listenPort.get().toString() + .trim() + } else { + "" + }, + mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "" ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/PeerProxy.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/PeerProxy.kt index 8306864..3b57ee7 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/PeerProxy.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/PeerProxy.kt @@ -3,30 +3,47 @@ package com.zaneschepke.wireguardautotunnel.ui.models import com.wireguard.config.Peer data class PeerProxy( - var publicKey : String = "", - var preSharedKey : String = "", - var persistentKeepalive : String = "", - var endpoint : String = "", + var publicKey: String = "", + var preSharedKey: String = "", + var persistentKeepalive: String = "", + var endpoint: String = "", var allowedIps: String = IPV4_WILDCARD.joinToString(", ").trim() -){ +) { companion object { - fun from(peer : Peer) : PeerProxy { + fun from(peer: Peer): PeerProxy { return PeerProxy( publicKey = peer.publicKey.toBase64(), - preSharedKey = if(peer.preSharedKey.isPresent) peer.preSharedKey.get().toBase64().trim() else "", - persistentKeepalive = if(peer.persistentKeepalive.isPresent) peer.persistentKeepalive.get().toString().trim() else "", - endpoint = if(peer.endpoint.isPresent) peer.endpoint.get().toString().trim() else "", + 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_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") } -} \ No newline at end of file +} 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 ba9a9a2..43ccfa6 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,7 +86,9 @@ import kotlinx.coroutines.launch import timber.log.Timber @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") -@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class, +@OptIn( + ExperimentalComposeUiApi::class, + ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class ) @Composable @@ -97,13 +99,11 @@ fun ConfigScreen( showSnackbarMessage: (String) -> Unit, id: String ) { - val context = LocalContext.current val scope = rememberCoroutineScope() val clipboardManager: ClipboardManager = LocalClipboardManager.current val keyboardController = LocalSoftwareKeyboardController.current - val tunnel by viewModel.tunnel.collectAsStateWithLifecycle(null) val tunnelName = viewModel.tunnelName.collectAsStateWithLifecycle() val packages by viewModel.packages.collectAsStateWithLifecycle() @@ -115,22 +115,25 @@ fun ConfigScreen( var showApplicationsDialog by remember { mutableStateOf(false) } var showAuthPrompt by remember { mutableStateOf(false) } var isAuthenticated by remember { mutableStateOf(false) } - val baseTextBoxModifier = Modifier.onFocusChanged { - if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { - keyboardController?.hide() + val baseTextBoxModifier = + Modifier.onFocusChanged { + if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { + keyboardController?.hide() + } } - } - val keyboardActions = KeyboardActions( - onDone = { - keyboardController?.hide() - } - ) + val keyboardActions = + KeyboardActions( + onDone = { + keyboardController?.hide() + } + ) - val keyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.None, - imeAction = ImeAction.Done - ) + val keyboardOptions = + KeyboardOptions( + capitalization = KeyboardCapitalization.None, + imeAction = ImeAction.Done + ) val fillMaxHeight = .85f val fillMaxWidth = .85f @@ -140,7 +143,7 @@ fun ConfigScreen( scope.launch(Dispatchers.IO) { try { viewModel.onScreenLoad(id) - } catch (e : Exception) { + } catch (e: Exception) { showSnackbarMessage(e.message!!) navController.navigate(Routes.Main.name) } @@ -149,29 +152,35 @@ fun ConfigScreen( val applicationButtonText = { "Tunneling apps: " + - if (isAllApplicationsEnabled) "all" - else "${checkedPackages.size} " + (if (include) "included" else "excluded") - + if (isAllApplicationsEnabled) { + "all" + } else { + "${checkedPackages.size} " + (if (include) "included" else "excluded") + } } - if(showAuthPrompt) { - AuthorizationPrompt(onSuccess = { - showAuthPrompt = false - isAuthenticated = true }, + if (showAuthPrompt) { + AuthorizationPrompt( + onSuccess = { + showAuthPrompt = false + isAuthenticated = true + }, onError = { error -> showSnackbarMessage(error) showAuthPrompt = false - }, + }, onFailure = { showAuthPrompt = false showSnackbarMessage(context.getString(R.string.authentication_failed)) - }) + } + ) } if (showApplicationsDialog) { - val sortedPackages = remember(packages) { - packages.sortedBy { viewModel.getPackageLabel(it) } - } + val sortedPackages = + remember(packages) { + packages.sortedBy { viewModel.getPackageLabel(it) } + } AlertDialog(onDismissRequest = { showApplicationsDialog = false }) { @@ -180,7 +189,8 @@ fun ConfigScreen( shadowElevation = 2.dp, shape = RoundedCornerShape(12.dp), color = MaterialTheme.colorScheme.surface, - modifier = Modifier + modifier = + Modifier .fillMaxWidth() .fillMaxHeight(if (isAllApplicationsEnabled) 1 / 5f else 4 / 5f) ) { @@ -188,7 +198,8 @@ fun ConfigScreen( modifier = Modifier.fillMaxWidth() ) { Row( - modifier = Modifier + modifier = + Modifier .fillMaxWidth() .padding(horizontal = 20.dp, vertical = 7.dp), verticalAlignment = Alignment.CenterVertically, @@ -204,7 +215,8 @@ fun ConfigScreen( } if (!isAllApplicationsEnabled) { Row( - modifier = Modifier + modifier = + Modifier .fillMaxWidth() .padding( horizontal = 20.dp, @@ -239,7 +251,8 @@ fun ConfigScreen( } } Row( - modifier = Modifier + modifier = + Modifier .fillMaxWidth() .padding( horizontal = 20.dp, @@ -254,21 +267,25 @@ fun ConfigScreen( LazyColumn( horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.Top, - modifier = Modifier + modifier = + Modifier .fillMaxHeight(4 / 5f) ) { items( sortedPackages, - key = { it.packageName }) { pack -> + key = { it.packageName } + ) { pack -> Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier + modifier = + Modifier .fillMaxSize() .padding(5.dp) ) { Row( - modifier = Modifier.fillMaxWidth( + modifier = + Modifier.fillMaxWidth( fillMaxWidth ) ) { @@ -278,11 +295,13 @@ fun ConfigScreen( ) if (drawable != null) { Image( - painter = DrawablePainter( + painter = + DrawablePainter( drawable ), stringResource(id = R.string.icon), - modifier = Modifier.size( + modifier = + Modifier.size( 50.dp, 50.dp ) @@ -291,7 +310,8 @@ fun ConfigScreen( Icon( Icons.Rounded.Android, stringResource(id = R.string.edit), - modifier = Modifier.size( + modifier = + Modifier.size( 50.dp, 50.dp ) @@ -306,11 +326,15 @@ fun ConfigScreen( modifier = Modifier.fillMaxSize(), checked = (checkedPackages.contains(pack.packageName)), onCheckedChange = { - if (it) viewModel.onAddCheckedPackage( - pack.packageName - ) else viewModel.onRemoveCheckedPackage( - pack.packageName - ) + if (it) { + viewModel.onAddCheckedPackage( + pack.packageName + ) + } else { + viewModel.onRemoveCheckedPackage( + pack.packageName + ) + } } ) } @@ -319,7 +343,8 @@ fun ConfigScreen( } Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier + modifier = + Modifier .fillMaxSize() .padding(top = 5.dp), horizontalArrangement = Arrangement.Center @@ -327,7 +352,8 @@ fun ConfigScreen( TextButton( onClick = { showApplicationsDialog = false - }) { + } + ) { Text(stringResource(R.string.done)) } } @@ -336,7 +362,6 @@ fun ConfigScreen( } } - if (tunnel != null) { Scaffold( floatingActionButtonPosition = FabPosition.End, @@ -345,37 +370,43 @@ fun ConfigScreen( val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) var fobColor by remember { mutableStateOf(secondaryColor) } FloatingActionButton( - modifier = Modifier.padding(bottom = 90.dp).onFocusChanged { - if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { - fobColor = if (it.isFocused) hoverColor else secondaryColor } + modifier = + Modifier.padding(bottom = 90.dp).onFocusChanged { + if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { + fobColor = if (it.isFocused) hoverColor else secondaryColor + } }, onClick = { scope.launch { try { viewModel.onSaveAllChanges() navController.navigate(Routes.Main.name) - showSnackbarMessage(context.resources.getString(R.string.config_changes_saved)) - } catch (e : Exception) { + showSnackbarMessage( + context.resources.getString(R.string.config_changes_saved) + ) + } catch (e: Exception) { Timber.e(e.message) showSnackbarMessage(e.message!!) } } }, containerColor = fobColor, - shape = RoundedCornerShape(16.dp), + shape = RoundedCornerShape(16.dp) ) { Icon( imageVector = Icons.Rounded.Save, contentDescription = stringResource(id = R.string.save_changes), - tint = Color.DarkGray, + tint = Color.DarkGray ) } - }) { + } + ) { Column { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top, - modifier = Modifier + modifier = + Modifier .verticalScroll(rememberScrollState()) .weight(1f, true) .fillMaxSize() @@ -385,21 +416,29 @@ fun ConfigScreen( shadowElevation = 2.dp, shape = RoundedCornerShape(12.dp), color = MaterialTheme.colorScheme.surface, - modifier = (if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) - Modifier - .fillMaxHeight(fillMaxHeight) - .fillMaxWidth(fillMaxWidth) - else Modifier.fillMaxWidth(fillMaxWidth)).padding( - top = 50.dp, - bottom = 10.dp - ) + modifier = + ( + if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { + Modifier + .fillMaxHeight(fillMaxHeight) + .fillMaxWidth(fillMaxWidth) + } else { + Modifier.fillMaxWidth(fillMaxWidth) + } + ).padding( + top = 50.dp, + bottom = 10.dp + ) ) { Column( horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.Top, modifier = Modifier.padding(15.dp).focusGroup() ) { - SectionTitle(stringResource(R.string.interface_), padding = screenPadding) + SectionTitle( + stringResource(R.string.interface_), + padding = screenPadding + ) ConfigurationTextBox( value = tunnelName.value, onValueChange = { value -> @@ -408,14 +447,17 @@ fun ConfigScreen( keyboardActions = keyboardActions, label = stringResource(R.string.name), hint = stringResource(R.string.tunnel_name).lowercase(), - modifier = baseTextBoxModifier.fillMaxWidth().focusRequester(focusRequester) + modifier = baseTextBoxModifier.fillMaxWidth().focusRequester( + focusRequester + ) ) OutlinedTextField( - modifier = baseTextBoxModifier.fillMaxWidth().clickable { + modifier = + baseTextBoxModifier.fillMaxWidth().clickable { showAuthPrompt = true }, value = proxyInterface.privateKey, - visualTransformation = if((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated) VisualTransformation.None else PasswordVisualTransformation(), + visualTransformation = if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated) VisualTransformation.None else PasswordVisualTransformation(), enabled = (id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated, onValueChange = { value -> viewModel.onPrivateKeyChange(value) @@ -425,7 +467,8 @@ fun ConfigScreen( modifier = Modifier.focusRequester(FocusRequester.Default), onClick = { viewModel.generateKeyPair() - }) { + } + ) { Icon( Icons.Rounded.Refresh, stringResource(R.string.rotate_keys), @@ -440,7 +483,9 @@ fun ConfigScreen( keyboardActions = keyboardActions ) OutlinedTextField( - modifier = baseTextBoxModifier.fillMaxWidth().focusRequester(FocusRequester.Default), + modifier = baseTextBoxModifier.fillMaxWidth().focusRequester( + FocusRequester.Default + ), value = proxyInterface.publicKey, enabled = false, onValueChange = {}, @@ -448,8 +493,11 @@ fun ConfigScreen( IconButton( modifier = Modifier.focusRequester(FocusRequester.Default), onClick = { - clipboardManager.setText(AnnotatedString(proxyInterface.publicKey)) - }) { + clipboardManager.setText( + AnnotatedString(proxyInterface.publicKey) + ) + } + ) { Icon( Icons.Rounded.ContentCopy, stringResource(R.string.copy_public_key), @@ -472,14 +520,15 @@ fun ConfigScreen( keyboardActions = keyboardActions, label = stringResource(R.string.addresses), hint = stringResource(R.string.comma_separated_list), - modifier = baseTextBoxModifier + modifier = + baseTextBoxModifier .fillMaxWidth(3 / 5f) .padding(end = 5.dp) ) ConfigurationTextBox( value = proxyInterface.listenPort, onValueChange = { value -> viewModel.onListenPortChanged(value) }, - keyboardActions = keyboardActions, + keyboardActions = keyboardActions, label = stringResource(R.string.listen_port), hint = stringResource(R.string.random), modifier = baseTextBoxModifier.width(IntrinsicSize.Min) @@ -492,7 +541,8 @@ fun ConfigScreen( keyboardActions = keyboardActions, label = stringResource(R.string.dns_servers), hint = stringResource(R.string.comma_separated_list), - modifier = baseTextBoxModifier + modifier = + baseTextBoxModifier .fillMaxWidth(3 / 5f) .padding(end = 5.dp) ) @@ -507,7 +557,8 @@ fun ConfigScreen( } Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier + modifier = + Modifier .fillMaxSize() .padding(top = 5.dp), horizontalArrangement = Arrangement.Center @@ -515,7 +566,8 @@ fun ConfigScreen( TextButton( onClick = { showApplicationsDialog = true - }) { + } + ) { Text(applicationButtonText()) } } @@ -527,30 +579,40 @@ fun ConfigScreen( shadowElevation = 2.dp, shape = RoundedCornerShape(12.dp), color = MaterialTheme.colorScheme.surface, - modifier = (if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) - Modifier - .fillMaxHeight(fillMaxHeight) - .fillMaxWidth(fillMaxWidth) - else Modifier.fillMaxWidth(fillMaxWidth)).padding( - top = 10.dp, - bottom = 10.dp - ) + modifier = + ( + if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { + 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 + modifier = + Modifier .padding(horizontal = 15.dp) .padding(bottom = 10.dp) ) { Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, - modifier = Modifier + modifier = + Modifier .fillMaxWidth() .padding(horizontal = 5.dp) ) { - SectionTitle(stringResource(R.string.peer), padding = screenPadding) + SectionTitle( + stringResource(R.string.peer), + padding = screenPadding + ) IconButton( onClick = { viewModel.onDeletePeer(index) @@ -593,10 +655,17 @@ fun ConfigScreen( onValueChange = { value -> viewModel.onPersistentKeepaliveChanged(index, value) }, - trailingIcon = { Text(stringResource(R.string.seconds), modifier = Modifier.padding(end = 10.dp)) }, + 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)) }, + placeholder = { + Text(stringResource(R.string.optional_no_recommend)) + }, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions ) @@ -625,7 +694,9 @@ fun ConfigScreen( }, label = { Text(stringResource(R.string.allowed_ips)) }, singleLine = true, - placeholder = { Text(stringResource(R.string.comma_separated_list)) }, + placeholder = { + Text(stringResource(R.string.comma_separated_list)) + }, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions ) @@ -635,11 +706,11 @@ fun ConfigScreen( Row( horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically, - modifier = Modifier + modifier = + Modifier .fillMaxSize() .padding(bottom = 140.dp) ) { - Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center @@ -647,16 +718,17 @@ fun ConfigScreen( TextButton( onClick = { viewModel.addEmptyPeer() - }) { + } + ) { Text(stringResource(R.string.add_peer)) } } } } - if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { + if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { Spacer(modifier = Modifier.weight(.17f)) } } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt index 6e56359..95481e1 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 @@ -23,18 +23,20 @@ import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.WgTunnelException import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import javax.inject.Inject @HiltViewModel -class ConfigViewModel @Inject constructor(private val application : Application, - private val tunnelRepo : TunnelConfigDao, - private val settingsRepo : SettingsDoa +class ConfigViewModel +@Inject +constructor( + private val application: Application, + private val tunnelRepo: TunnelConfigDao, + private val settingsRepo: SettingsDoa ) : ViewModel() { - private val _tunnel = MutableStateFlow(null) private val _tunnelName = MutableStateFlow("") val tunnelName get() = _tunnelName.asStateFlow() @@ -58,13 +60,14 @@ class ConfigViewModel @Inject constructor(private val application : Application, private val _isAllApplicationsEnabled = MutableStateFlow(false) val isAllApplicationsEnabled get() = _isAllApplicationsEnabled.asStateFlow() private val _isDefaultTunnel = MutableStateFlow(false) - val isDefaultTunnel = _isDefaultTunnel.asStateFlow() private lateinit var tunnelConfig: TunnelConfig - suspend fun onScreenLoad(id : String) { - if(id != Constants.MANUAL_TUNNEL_CONFIG_ID) { - tunnelConfig = getTunnelConfigById(id) ?: throw WgTunnelException("Config not found") + suspend fun onScreenLoad(id: String) { + if (id != Constants.MANUAL_TUNNEL_CONFIG_ID) { + tunnelConfig = getTunnelConfigById(id) ?: throw WgTunnelException( + "Config not found" + ) emitScreenData() } else { emitEmptyScreenData() @@ -84,7 +87,6 @@ class ConfigViewModel @Inject constructor(private val application : Application, } } - private suspend fun emitScreenData() { emitTunnelConfig() emitPeersFromConfig() @@ -97,7 +99,7 @@ class ConfigViewModel @Inject constructor(private val application : Application, private suspend fun emitDefaultTunnelStatus() { val settings = settingsRepo.getAll() - if(settings.isNotEmpty()) { + if (settings.isNotEmpty()) { _isDefaultTunnel.value = settings.first().isTunnelConfigDefault(tunnelConfig) } } @@ -109,7 +111,7 @@ class ConfigViewModel @Inject constructor(private val application : Application, private fun emitPeersFromConfig() { val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick) - config.peers.forEach{ + config.peers.forEach { _proxyPeers.value.add(PeerProxy.from(it)) } } @@ -122,10 +124,10 @@ class ConfigViewModel @Inject constructor(private val application : Application, _interface.value = interfaceProxy } - private suspend fun getTunnelConfigById(id : String) : TunnelConfig? { + private suspend fun getTunnelConfigById(id: String): TunnelConfig? { return try { tunnelRepo.getById(id.toLong()) - } catch (_ : Exception) { + } catch (_: Exception) { null } } @@ -134,30 +136,31 @@ class ConfigViewModel @Inject constructor(private val application : Application, _tunnel.emit(tunnelConfig) } - private suspend fun emitTunnelConfigName() { + private suspend fun emitTunnelConfigName() { _tunnelName.emit(tunnelConfig.name) } - fun onTunnelNameChange(name : String) { + fun onTunnelNameChange(name: String) { _tunnelName.value = name } - fun onIncludeChange(include : Boolean) { + fun onIncludeChange(include: Boolean) { _include.value = include } - fun onAddCheckedPackage(packageName : String) { + + fun onAddCheckedPackage(packageName: String) { _checkedPackages.value.add(packageName) } - fun onAllApplicationsChange(isAllApplicationsEnabled : Boolean) { + fun onAllApplicationsChange(isAllApplicationsEnabled: Boolean) { _isAllApplicationsEnabled.value = isAllApplicationsEnabled } - fun onRemoveCheckedPackage(packageName : String) { + fun onRemoveCheckedPackage(packageName: String) { _checkedPackages.value.remove(packageName) } - private suspend fun emitSplitTunnelConfiguration(config : Config) { + private suspend fun emitSplitTunnelConfiguration(config: Config) { val excludedApps = config.`interface`.excludedApplications val includedApps = config.`interface`.includedApplications if (excludedApps.isNotEmpty() || includedApps.isNotEmpty()) { @@ -168,7 +171,10 @@ class ConfigViewModel @Inject constructor(private val application : Application, } } - private suspend fun determineAppInclusionState(excludedApps : Set, includedApps : Set) { + private suspend fun determineAppInclusionState( + excludedApps: Set, + includedApps: Set + ) { if (excludedApps.isEmpty()) { emitIncludedAppsExist() emitCheckedApps(includedApps) @@ -186,7 +192,7 @@ class ConfigViewModel @Inject constructor(private val application : Application, _include.emit(false) } - private suspend fun emitCheckedApps(apps : Set) { + private suspend fun emitCheckedApps(apps: Set) { _checkedPackages.emit(apps.toMutableStateList()) } @@ -205,45 +211,45 @@ class ConfigViewModel @Inject constructor(private val application : Application, } } - fun emitQueriedPackages(query : String) { + fun emitQueriedPackages(query: String) { viewModelScope.launch(Dispatchers.IO) { - val packages = getAllInternetCapablePackages().filter { - getPackageLabel(it).lowercase().contains(query.lowercase()) - } + val packages = + getAllInternetCapablePackages().filter { + getPackageLabel(it).lowercase().contains(query.lowercase()) + } _packages.emit(packages) } } - fun getPackageLabel(packageInfo : PackageInfo) : String { + fun getPackageLabel(packageInfo: PackageInfo): String { return packageInfo.applicationInfo.loadLabel(application.packageManager).toString() } - - private fun getAllInternetCapablePackages() : List { + 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)) + packageManager.getPackagesHoldingPermissions( + permissions, + PackageManager.PackageInfoFlags.of(0L) + ) } else { packageManager.getPackagesHoldingPermissions(permissions, 0) } } - private fun isAllApplicationsEnabled() : Boolean { + private fun isAllApplicationsEnabled(): Boolean { return _isAllApplicationsEnabled.value } - private fun isIncludeApplicationsEnabled() : Boolean { - return _include.value - } - private suspend fun saveConfig(tunnelConfig: TunnelConfig) { tunnelRepo.save(tunnelConfig) } + private suspend fun updateTunnelConfig(tunnelConfig: TunnelConfig?) { - if(tunnelConfig != null) { + if (tunnelConfig != null) { saveConfig(tunnelConfig) updateSettingsDefaultTunnel(tunnelConfig) } @@ -251,88 +257,119 @@ class ConfigViewModel @Inject constructor(private val application : Application, private suspend fun updateSettingsDefaultTunnel(tunnelConfig: TunnelConfig) { val settings = settingsRepo.getAll() - if(settings.isNotEmpty()) { + if (settings.isNotEmpty()) { val setting = settings[0] - if(setting.defaultTunnel != null) { - if(tunnelConfig.id == TunnelConfig.from(setting.defaultTunnel!!).id) { - settingsRepo.save(setting.copy( - defaultTunnel = tunnelConfig.toString() - )) + if (setting.defaultTunnel != null) { + if (tunnelConfig.id == TunnelConfig.from(setting.defaultTunnel!!).id) { + settingsRepo.save( + setting.copy( + defaultTunnel = tunnelConfig.toString() + ) + ) } } } } - fun buildPeerListFromProxyPeers() : List { + private fun buildPeerListFromProxyPeers(): List { return _proxyPeers.value.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()) + if (it.persistentKeepalive.isNotEmpty()) { + builder.parsePersistentKeepalive( + it.persistentKeepalive.trim() + ) + } builder.build() } } - fun buildInterfaceListFromProxyInterface() : Interface { + private fun buildInterfaceListFromProxyInterface(): Interface { val builder = Interface.Builder() builder.parsePrivateKey(_interface.value.privateKey.trim()) builder.parseAddresses(_interface.value.addresses.trim()) builder.parseDnsServers(_interface.value.dnsServers.trim()) - if(_interface.value.mtu.isNotEmpty()) builder.parseMtu(_interface.value.mtu.trim()) - if(_interface.value.listenPort.isNotEmpty()) builder.parseListenPort(_interface.value.listenPort.trim()) - if(isAllApplicationsEnabled()) _checkedPackages.value.clear() - if(_include.value) builder.includeApplications(_checkedPackages.value) - if(!_include.value) builder.excludeApplications(_checkedPackages.value) + if (_interface.value.mtu.isNotEmpty()) builder.parseMtu(_interface.value.mtu.trim()) + if (_interface.value.listenPort.isNotEmpty()) { + builder.parseListenPort( + _interface.value.listenPort.trim() + ) + } + if (isAllApplicationsEnabled()) _checkedPackages.value.clear() + if (_include.value) builder.includeApplications(_checkedPackages.value) + if (!_include.value) builder.excludeApplications(_checkedPackages.value) return builder.build() } - - suspend fun onSaveAllChanges() { try { val peerList = buildPeerListFromProxyPeers() val wgInterface = buildInterfaceListFromProxyInterface() val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build() - val tunnelConfig = _tunnel.value?.copy( - name = _tunnelName.value, - wgQuick = config.toWgQuickString() - ) + val tunnelConfig = + _tunnel.value?.copy( + name = _tunnelName.value, + wgQuick = config.toWgQuickString() + ) updateTunnelConfig(tunnelConfig) - } catch (e : Exception) { - throw WgTunnelException("Error: ${e.cause?.message?.lowercase() ?: "unknown error occurred"}") + } catch (e: Exception) { + throw WgTunnelException( + "Error: ${e.cause?.message?.lowercase() ?: "unknown error occurred"}" + ) } } - fun onPeerPublicKeyChange(index: Int, publicKey: String) { - _proxyPeers.value[index] = _proxyPeers.value[index].copy( - publicKey = publicKey - ) + fun onPeerPublicKeyChange( + index: Int, + publicKey: String + ) { + _proxyPeers.value[index] = + _proxyPeers.value[index].copy( + publicKey = publicKey + ) } - fun onPreSharedKeyChange(index: Int, value: String) { - _proxyPeers.value[index] = _proxyPeers.value[index].copy( - preSharedKey = value - ) + fun onPreSharedKeyChange( + index: Int, + value: String + ) { + _proxyPeers.value[index] = + _proxyPeers.value[index].copy( + preSharedKey = value + ) } - fun onEndpointChange(index: Int, value: String) { - _proxyPeers.value[index] = _proxyPeers.value[index].copy( - endpoint = value - ) + fun onEndpointChange( + index: Int, + value: String + ) { + _proxyPeers.value[index] = + _proxyPeers.value[index].copy( + endpoint = value + ) } - fun onAllowedIpsChange(index: Int, value: String) { - _proxyPeers.value[index] = _proxyPeers.value[index].copy( - allowedIps = value - ) + fun onAllowedIpsChange( + index: Int, + value: String + ) { + _proxyPeers.value[index] = + _proxyPeers.value[index].copy( + allowedIps = value + ) } - fun onPersistentKeepaliveChanged(index : Int, value : String) { - _proxyPeers.value[index] = _proxyPeers.value[index].copy( - persistentKeepalive = value - ) + fun onPersistentKeepaliveChanged( + index: Int, + value: String + ) { + _proxyPeers.value[index] = + _proxyPeers.value[index].copy( + persistentKeepalive = value + ) } fun onDeletePeer(index: Int) { @@ -345,51 +382,58 @@ class ConfigViewModel @Inject constructor(private val application : Application, fun generateKeyPair() { val keyPair = KeyPair() - _interface.value = _interface.value.copy( - privateKey = keyPair.privateKey.toBase64(), - publicKey = keyPair.publicKey.toBase64() - ) + _interface.value = + _interface.value.copy( + privateKey = keyPair.privateKey.toBase64(), + publicKey = keyPair.publicKey.toBase64() + ) } fun onAddressesChanged(value: String) { - _interface.value = _interface.value.copy( - addresses = value - ) + _interface.value = + _interface.value.copy( + addresses = value + ) } fun onListenPortChanged(value: String) { - _interface.value = _interface.value.copy( - listenPort = value - ) + _interface.value = + _interface.value.copy( + listenPort = value + ) } fun onDnsServersChanged(value: String) { - _interface.value = _interface.value.copy( - dnsServers = value - ) + _interface.value = + _interface.value.copy( + dnsServers = value + ) } fun onMtuChanged(value: String) { - _interface.value = _interface.value.copy( - mtu = value - ) + _interface.value = + _interface.value.copy( + mtu = value + ) } - private fun onInterfacePublicKeyChange(value : String) { - _interface.value = _interface.value.copy( - publicKey = value - ) + private fun onInterfacePublicKeyChange(value: String) { + _interface.value = + _interface.value.copy( + publicKey = value + ) } fun onPrivateKeyChange(value: String) { - _interface.value = _interface.value.copy( - privateKey = value - ) - if(NumberUtils.isValidKey(value)) { + _interface.value = + _interface.value.copy( + privateKey = value + ) + if (NumberUtils.isValidKey(value)) { val pair = KeyPair(Key.fromBase64(value)) onInterfacePublicKeyChange(pair.publicKey.toBase64()) } else { onInterfacePublicKeyChange("") } } -} \ No newline at end of file +} 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 e66e843..3649c5c 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 @@ -88,6 +88,7 @@ import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait import com.zaneschepke.wireguardautotunnel.ui.Routes import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem import com.zaneschepke.wireguardautotunnel.ui.theme.brickRed +import com.zaneschepke.wireguardautotunnel.ui.theme.corn import com.zaneschepke.wireguardautotunnel.ui.theme.mint import com.zaneschepke.wireguardautotunnel.util.WgTunnelException import kotlinx.coroutines.Dispatchers @@ -102,7 +103,6 @@ fun MainScreen( showSnackbarMessage: (String) -> Unit, navController: NavController ) { - val haptic = LocalHapticFeedback.current val context = LocalContext.current val isVisible = rememberSaveable { mutableStateOf(true) } @@ -112,7 +112,9 @@ fun MainScreen( var showBottomSheet by remember { mutableStateOf(false) } var showPrimaryChangeAlertDialog by remember { mutableStateOf(false) } val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf()) - val handshakeStatus by viewModel.handshakeStatus.collectAsStateWithLifecycle(HandshakeStatus.NOT_STARTED) + val handshakeStatus by viewModel.handshakeStatus.collectAsStateWithLifecycle( + HandshakeStatus.NOT_STARTED + ) var selectedTunnel by remember { mutableStateOf(null) } val state by viewModel.state.collectAsStateWithLifecycle(Tunnel.State.DOWN) val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle("") @@ -120,75 +122,100 @@ fun MainScreen( val statistics by viewModel.statistics.collectAsStateWithLifecycle(null) // 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 + 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 } - // Show FAB - if (available.y > 1) { - isVisible.value = true + } + } + + 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 + ) + } + ) { + throw WgTunnelException(context.getString(R.string.no_file_explorer)) + } + return intent } - return Offset.Zero } - } - } - - 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) - }) { - throw WgTunnelException(context.getString(R.string.no_file_explorer)) - } - return intent - } - }) { data -> - if (data == null) return@rememberLauncherForActivityResult - scope.launch(Dispatchers.IO) { - try { - viewModel.onTunnelFileSelected(data) - } catch (e : WgTunnelException) { - showSnackbarMessage(e.message) - } - } - } - - val scanLauncher = rememberLauncherForActivityResult( - contract = ScanContract(), - onResult = { - scope.launch { + ) { data -> + if (data == null) return@rememberLauncherForActivityResult + scope.launch(Dispatchers.IO) { try { - viewModel.onTunnelQrResult(it.contents) - } catch (e: Exception) { - when(e) { - is WgTunnelException -> { - showSnackbarMessage(e.message) - } else -> { - showSnackbarMessage("No QR code scanned") + viewModel.onTunnelFileSelected(data) + } catch (e: WgTunnelException) { + showSnackbarMessage(e.message) + } + } + } + + val scanLauncher = + rememberLauncherForActivityResult( + contract = ScanContract(), + onResult = { + scope.launch { + try { + viewModel.onTunnelQrResult(it.contents) + } catch (e: Exception) { + when (e) { + is WgTunnelException -> { + showSnackbarMessage(e.message) + } + + else -> { + showSnackbarMessage("No QR code scanned") + } } } } } - } - ) + ) - if(showPrimaryChangeAlertDialog) { + if (showPrimaryChangeAlertDialog) { AlertDialog( onDismissRequest = { - showPrimaryChangeAlertDialog = false + showPrimaryChangeAlertDialog = false }, confirmButton = { TextButton(onClick = { @@ -197,30 +224,32 @@ fun MainScreen( showPrimaryChangeAlertDialog = false selectedTunnel = null } - }) - { Text(text = stringResource(R.string.okay)) } + }) { Text(text = stringResource(R.string.okay)) } }, dismissButton = { TextButton(onClick = { showPrimaryChangeAlertDialog = false - }) - { Text(text = stringResource(R.string.cancel)) } + }) { Text(text = stringResource(R.string.cancel)) } }, title = { Text(text = stringResource(R.string.primary_tunnel_change)) }, text = { Text(text = stringResource(R.string.primary_tunnel_change_question)) } ) } - fun onTunnelToggle(checked : Boolean , tunnel : TunnelConfig) { + fun onTunnelToggle( + checked: Boolean, + tunnel: TunnelConfig + ) { try { if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop() - } catch (e : Exception) { + } catch (e: Exception) { showSnackbarMessage(e.message!!) } } Scaffold( - modifier = Modifier.pointerInput(Unit) { + modifier = + Modifier.pointerInput(Unit) { detectTapGestures(onTap = { selectedTunnel = null }) @@ -230,30 +259,30 @@ fun MainScreen( AnimatedVisibility( visible = isVisible.value, enter = slideInVertically(initialOffsetY = { it * 2 }), - exit = slideOutVertically(targetOffsetY = { it * 2 }), + exit = slideOutVertically(targetOffsetY = { it * 2 }) ) { val secondaryColor = MaterialTheme.colorScheme.secondary val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) var fobColor by remember { mutableStateOf(secondaryColor) } FloatingActionButton( - modifier = Modifier + modifier = + Modifier .padding(bottom = 90.dp) .onFocusChanged { if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { fobColor = if (it.isFocused) hoverColor else secondaryColor } - } - , + }, onClick = { showBottomSheet = true }, containerColor = fobColor, - shape = RoundedCornerShape(16.dp), + shape = RoundedCornerShape(16.dp) ) { Icon( imageVector = Icons.Rounded.Add, contentDescription = stringResource(id = R.string.add_tunnel), - tint = Color.DarkGray, + tint = Color.DarkGray ) } } @@ -263,7 +292,8 @@ fun MainScreen( Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, - modifier = Modifier + modifier = + Modifier .fillMaxSize() .padding(padding) ) { @@ -279,7 +309,8 @@ fun MainScreen( ) { // Sheet content Row( - modifier = Modifier + modifier = + Modifier .fillMaxWidth() .clickable { showBottomSheet = false @@ -301,23 +332,26 @@ fun MainScreen( modifier = Modifier.padding(10.dp) ) } - if(!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { + if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { Divider() - Row(modifier = Modifier - .fillMaxWidth() - .clickable { - scope.launch { - showBottomSheet = false - val scanOptions = ScanOptions() - scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE) - scanOptions.setOrientationLocked(true) - scanOptions.setPrompt(context.getString(R.string.scanning_qr)) - scanOptions.setBeepEnabled(false) - scanOptions.captureActivity = CaptureActivityPortrait::class.java - scanLauncher.launch(scanOptions) + Row( + modifier = + Modifier + .fillMaxWidth() + .clickable { + scope.launch { + showBottomSheet = false + val scanOptions = ScanOptions() + scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE) + scanOptions.setOrientationLocked(true) + scanOptions.setPrompt(context.getString(R.string.scanning_qr)) + scanOptions.setBeepEnabled(false) + scanOptions.captureActivity = + CaptureActivityPortrait::class.java + scanLauncher.launch(scanOptions) + } } - } - .padding(10.dp) + .padding(10.dp) ) { Icon( Icons.Filled.QrCode, @@ -332,11 +366,14 @@ fun MainScreen( } Divider() Row( - modifier = Modifier + modifier = + Modifier .fillMaxWidth() .clickable { showBottomSheet = false - navController.navigate("${Routes.Config.name}/${Constants.MANUAL_TUNNEL_CONFIG_ID}") + navController.navigate( + "${Routes.Config.name}/${Constants.MANUAL_TUNNEL_CONFIG_ID}" + ) } .padding(10.dp) ) { @@ -355,47 +392,67 @@ fun MainScreen( Column( horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.Top, - modifier = Modifier + modifier = + Modifier .fillMaxSize() .padding(padding) ) { LazyColumn( - modifier = Modifier + modifier = + Modifier .fillMaxSize() - .nestedScroll(nestedScrollConnection), + .padding(top = 10.dp) + .nestedScroll(nestedScrollConnection) ) { items(tunnels, key = { tunnel -> tunnel.id }) { tunnel -> - val leadingIconColor = (if (tunnelName == tunnel.name) when (handshakeStatus) { - HandshakeStatus.HEALTHY -> mint - HandshakeStatus.UNHEALTHY -> brickRed - HandshakeStatus.NOT_STARTED -> Color.Gray - HandshakeStatus.NEVER_CONNECTED -> brickRed - } else {Color.Gray}) - val focusRequester = remember { FocusRequester() } - val expanded = remember { - mutableStateOf(false) - } - RowListItem(icon = { - if (settings.isTunnelConfigDefault(tunnel)) - Icon( - Icons.Rounded.Star, stringResource(R.string.status), - tint = leadingIconColor, - modifier = Modifier - .padding(end = 10.dp) - .size(20.dp) + val leadingIconColor = ( + if (tunnelName == tunnel.name) { + when (handshakeStatus) { + HandshakeStatus.HEALTHY -> mint + HandshakeStatus.UNHEALTHY -> brickRed + HandshakeStatus.STALE -> corn + HandshakeStatus.NOT_STARTED -> Color.Gray + HandshakeStatus.NEVER_CONNECTED -> brickRed + } + } else { + Color.Gray + } ) - else Icon( - Icons.Rounded.Circle, stringResource(R.string.status), - tint = leadingIconColor, - modifier = Modifier - .padding(end = 15.dp) - .size(15.dp) - ) - }, + val focusRequester = remember { FocusRequester() } + val expanded = + remember { + mutableStateOf(false) + } + RowListItem( + icon = { + if (settings.isTunnelConfigDefault(tunnel)) { + Icon( + Icons.Rounded.Star, + stringResource(R.string.status), + tint = leadingIconColor, + modifier = + Modifier + .padding(end = 10.dp) + .size(20.dp) + ) + } else { + Icon( + Icons.Rounded.Circle, + stringResource(R.string.status), + tint = leadingIconColor, + modifier = + Modifier + .padding(end = 15.dp) + .size(15.dp) + ) + } + }, text = tunnel.name, onHold = { - if (state == Tunnel.State.UP && tunnel.name == tunnelName) { - showSnackbarMessage(context.resources.getString(R.string.turn_off_tunnel)) + if ((state == Tunnel.State.UP) && (tunnel.name == tunnelName)) { + showSnackbarMessage( + context.resources.getString(R.string.turn_off_tunnel) + ) return@RowListItem } haptic.performHapticFeedback(HapticFeedbackType.LongPress) @@ -403,7 +460,7 @@ fun MainScreen( }, onClick = { if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { - if(state == Tunnel.State.UP && (tunnelName == tunnel.name) ) { + if (state == Tunnel.State.UP && (tunnelName == tunnel.name)) { expanded.value = !expanded.value } } else { @@ -414,25 +471,40 @@ fun MainScreen( statistics = statistics, expanded = expanded.value, rowButton = { - if (tunnel.id == selectedTunnel?.id && !WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { + if (tunnel.id == selectedTunnel?.id && !WireGuardAutoTunnel.isRunningOnAndroidTv( + context + ) + ) { Row { - if(!settings.isTunnelConfigDefault(tunnel)) { + if (!settings.isTunnelConfigDefault(tunnel)) { IconButton(onClick = { - if(settings.isAutoTunnelEnabled) { - showSnackbarMessage(context.resources.getString(R.string.turn_off_auto)) - } else showPrimaryChangeAlertDialog = true + if (settings.isAutoTunnelEnabled) { + showSnackbarMessage( + context.resources.getString( + R.string.turn_off_auto + ) + ) + } else { + showPrimaryChangeAlertDialog = true + } }) { - Icon(Icons.Rounded.Star, stringResource(id = R.string.set_primary)) + Icon( + Icons.Rounded.Star, + stringResource(id = R.string.set_primary) + ) } } IconButton(onClick = { - navController.navigate("${Routes.Config.name}/${selectedTunnel?.id}") + navController.navigate( + "${Routes.Config.name}/${selectedTunnel?.id}" + ) }) { Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit)) } IconButton( modifier = Modifier.focusable(), - onClick = { viewModel.onDelete(tunnel) }) { + onClick = { viewModel.onDelete(tunnel) } + ) { Icon( Icons.Rounded.Delete, stringResource(id = R.string.delete) @@ -441,45 +513,59 @@ fun MainScreen( } } else { val checked = state == Tunnel.State.UP && tunnel.name == tunnelName - if(!checked) expanded.value = false + if (!checked) expanded.value = false + @Composable - fun TunnelSwitch() = Switch( - modifier = Modifier.focusRequester(focusRequester), - checked = checked, - onCheckedChange = { checked -> - if(!checked) expanded.value = false - onTunnelToggle(checked, tunnel) - } - ) + fun TunnelSwitch() = + Switch( + modifier = Modifier.focusRequester(focusRequester), + checked = checked, + onCheckedChange = { checked -> + if (!checked) expanded.value = false + onTunnelToggle(checked, tunnel) + } + ) if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { Row { - if(!settings.isTunnelConfigDefault(tunnel)) { + if (!settings.isTunnelConfigDefault(tunnel)) { IconButton(onClick = { - if(settings.isAutoTunnelEnabled) { - showSnackbarMessage(context.resources.getString(R.string.turn_off_auto)) - } else showPrimaryChangeAlertDialog = true + if (settings.isAutoTunnelEnabled) { + showSnackbarMessage( + context.resources.getString( + R.string.turn_off_auto + ) + ) + } else { + showPrimaryChangeAlertDialog = true + } }) { - Icon(Icons.Rounded.Star, stringResource(id = R.string.set_primary)) + Icon( + Icons.Rounded.Star, + stringResource(id = R.string.set_primary) + ) } } IconButton( modifier = Modifier.focusRequester(focusRequester), onClick = { - if(state == Tunnel.State.UP && (tunnelName == tunnel.name) ) { + if (state == Tunnel.State.UP && (tunnelName == tunnel.name)) { expanded.value = !expanded.value } - }) { + } + ) { Icon(Icons.Rounded.Info, stringResource(R.string.info)) } IconButton(onClick = { - if (state == Tunnel.State.UP && tunnel.name == tunnelName) + if (state == Tunnel.State.UP && tunnel.name == tunnelName) { showSnackbarMessage( context.resources.getString( R.string.turn_off_tunnel ) ) - else { - navController.navigate("${Routes.Config.name}/${tunnel.id}") + } else { + navController.navigate( + "${Routes.Config.name}/${tunnel.id}" + ) } }) { Icon( @@ -488,13 +574,13 @@ fun MainScreen( ) } IconButton(onClick = { - if (state == Tunnel.State.UP && tunnel.name == tunnelName) + if (state == Tunnel.State.UP && tunnel.name == tunnelName) { showSnackbarMessage( context.resources.getString( R.string.turn_off_tunnel ) ) - else { + } else { viewModel.onDelete(tunnel) } }) { @@ -509,7 +595,8 @@ fun MainScreen( TunnelSwitch() } } - }) + } + ) } } } 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 a1919be..b0454a9 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 @@ -22,6 +22,9 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.WgTunnelException import dagger.hilt.android.lifecycle.HiltViewModel +import java.io.InputStream +import java.util.zip.ZipInputStream +import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -29,19 +32,16 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.io.InputStream -import java.util.zip.ZipInputStream -import javax.inject.Inject - @HiltViewModel -class MainViewModel @Inject constructor( +class MainViewModel +@Inject +constructor( private val application: Application, private val tunnelRepo: TunnelConfigDao, private val settingsRepo: SettingsDoa, private val vpnService: VpnService ) : ViewModel() { - val tunnels get() = tunnelRepo.getAllFlow() val state get() = vpnService.state @@ -62,10 +62,11 @@ class MainViewModel @Inject constructor( } private fun validateWatcherServiceState(settings: Settings) { - val watcherState = ServiceManager.getServiceState( - application.applicationContext, - WireGuardConnectivityWatcherService::class.java - ) + val watcherState = + ServiceManager.getServiceState( + application.applicationContext, + WireGuardConnectivityWatcherService::class.java + ) if (settings.isAutoTunnelEnabled && watcherState == ServiceState.STOPPED && settings.defaultTunnel != null) { ServiceManager.startWatcherService( application.applicationContext, @@ -74,7 +75,6 @@ class MainViewModel @Inject constructor( } } - fun onDelete(tunnel: TunnelConfig) { viewModelScope.launch { if (tunnelRepo.count() == 1L) { @@ -106,7 +106,7 @@ class MainViewModel @Inject constructor( private suspend fun stopActiveTunnel() { if (ServiceManager.getServiceState( application.applicationContext, - WireGuardTunnelService::class.java, + WireGuardTunnelService::class.java ) == ServiceState.STARTED ) { onTunnelStop() @@ -128,12 +128,15 @@ class MainViewModel @Inject constructor( val tunnelConfig = TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result) addTunnel(tunnelConfig) - } catch (e : Exception) { + } catch (e: Exception) { throw WgTunnelException(e) } } - private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) { + private suspend fun saveTunnelConfigFromStream( + stream: InputStream, + fileName: String + ) { val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8) val config = Config.parse(bufferReader) val tunnelName = getNameFromFileName(fileName) @@ -152,10 +155,12 @@ class MainViewModel @Inject constructor( try { val fileName = getFileName(application.applicationContext, uri) val fileExtension = getFileExtensionFromFileName(fileName) - when(fileExtension){ + when (fileExtension) { Constants.CONF_FILE_EXTENSION -> saveTunnelFromConfUri(fileName, uri) Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri) - else -> throw WgTunnelException(application.getString(R.string.file_extension_message)) + else -> throw WgTunnelException( + application.getString(R.string.file_extension_message) + ) } } catch (e: Exception) { throw WgTunnelException(e) @@ -165,19 +170,24 @@ class MainViewModel @Inject constructor( private suspend fun saveTunnelsFromZipUri(uri: Uri) { ZipInputStream(getInputStreamFromUri(uri)).use { zip -> generateSequence { zip.nextEntry } - .filterNot { it.isDirectory || - getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION } + .filterNot { + it.isDirectory || + getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION + } .forEach { val name = getNameFromFileName(it.name) val config = Config.parse(zip) viewModelScope.launch(Dispatchers.IO) { addTunnel(TunnelConfig(name = name, wgQuick = config.toWgQuickString())) + } } - } } } - private suspend fun saveTunnelFromConfUri(name : String, uri: Uri) { + private suspend fun saveTunnelFromConfUri( + name: String, + uri: Uri + ) { val stream = getInputStreamFromUri(uri) saveTunnelConfigFromStream(stream, name) } @@ -190,7 +200,10 @@ class MainViewModel @Inject constructor( tunnelRepo.save(tunnelConfig) } - private fun getFileNameByCursor(context: Context, uri: Uri): String { + private fun getFileNameByCursor( + context: Context, + uri: Uri + ): String { val cursor = context.contentResolver.query(uri, null, null, null, null) if (cursor != null) { cursor.use { @@ -224,8 +237,10 @@ class MainViewModel @Inject constructor( } } - - private fun getFileName(context: Context, uri: Uri): String { + private fun getFileName( + context: Context, + uri: Uri + ): String { validateUriContentScheme(uri) return try { getFileNameByCursor(context, uri) @@ -256,4 +271,4 @@ class MainViewModel @Inject constructor( settingsRepo.save(_settings.value) } } -} \ No newline at end of file +} 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 de88650..7250ffc 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 @@ -38,6 +38,7 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -65,29 +66,32 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState +import com.wireguard.android.backend.Tunnel +import com.wireguard.android.backend.WgQuickBackend import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel +import com.zaneschepke.wireguardautotunnel.repository.datastore.DataStoreManager import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle -import com.zaneschepke.wireguardautotunnel.util.StorageUtil +import com.zaneschepke.wireguardautotunnel.util.FileUtils +import com.zaneschepke.wireguardautotunnel.util.WgTunnelException +import java.io.File import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import java.io.File - @OptIn( ExperimentalPermissionsApi::class, - ExperimentalLayoutApi::class, ExperimentalComposeUiApi::class + ExperimentalLayoutApi::class, + ExperimentalComposeUiApi::class ) @Composable fun SettingsScreen( viewModel: SettingsViewModel = hiltViewModel(), padding: PaddingValues, showSnackbarMessage: (String) -> Unit, - focusRequester: FocusRequester, + focusRequester: FocusRequester ) { - val scope = rememberCoroutineScope { Dispatchers.IO } val context = LocalContext.current val focusManager = LocalFocusManager.current @@ -100,16 +104,24 @@ fun SettingsScreen( val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION) var currentText by remember { mutableStateOf("") } val scrollState = rememberScrollState() - var isLocationDisclaimerNeeded by remember { mutableStateOf(true) } var isBackgroundLocationGranted by remember { mutableStateOf(true) } var showAuthPrompt by remember { mutableStateOf(false) } var didExportFiles by remember { mutableStateOf(false) } - - + val isLocationDisclosureShown by viewModel.disclosureShown.collectAsStateWithLifecycle( + null + ) + val vpnState = viewModel.vpnState.collectAsStateWithLifecycle(initialValue = Tunnel.State.DOWN) val screenPadding = 5.dp val fillMaxWidth = .85f + fun setLocationDisclosureShown() = scope.launch { + viewModel.dataStoreManager.saveToDataStore( + DataStoreManager.LOCATION_DISCLOSURE_SHOWN, + true + ) + } + fun exportAllConfigs() { try { val files = tunnels.map { File(context.cacheDir, "${it.name}.conf") } @@ -118,33 +130,35 @@ fun SettingsScreen( it.write(tunnels[index].wgQuick.toByteArray()) } } - StorageUtil.saveFilesToZip(context, files) + FileUtils.saveFilesToZip(context, files) didExportFiles = true showSnackbarMessage(context.getString(R.string.exported_configs_message)) - } catch (e : Exception) { + } catch (e: Exception) { showSnackbarMessage(e.message!!) } } - fun saveTrustedSSID() { if (currentText.isNotEmpty()) { scope.launch { try { viewModel.onSaveTrustedSSID(currentText) currentText = "" - } catch (e : Exception) { + } catch (e: Exception) { showSnackbarMessage(e.message ?: context.getString(R.string.unknown_error)) } } } } - fun isAllAutoTunnelPermissionsEnabled() : Boolean { - return(isBackgroundLocationGranted && fineLocationState.status.isGranted && !viewModel.isLocationServicesNeeded()) + fun isAllAutoTunnelPermissionsEnabled(): Boolean { + return ( + isBackgroundLocationGranted && + fineLocationState.status.isGranted && + !viewModel.isLocationServicesNeeded() + ) } - fun openSettings() { scope.launch { val intentSettings = @@ -155,334 +169,428 @@ fun SettingsScreen( } } - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val backgroundLocationState = rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION) - if(!backgroundLocationState.status.isGranted) { + isBackgroundLocationGranted = if (!backgroundLocationState.status.isGranted) { + false + } else { + SideEffect { + setLocationDisclosureShown() + } + true + } + } + + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { + if (!fineLocationState.status.isGranted) { isBackgroundLocationGranted = false } else { - isLocationDisclaimerNeeded = false + SideEffect { + setLocationDisclosureShown() + } isBackgroundLocationGranted = true } } - if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { - if(!fineLocationState.status.isGranted) { - isBackgroundLocationGranted = false - } else { - isLocationDisclaimerNeeded = false - isBackgroundLocationGranted = true + if (isLocationDisclosureShown != true) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + modifier = + Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .padding(padding) + ) { + Icon( + Icons.Rounded.LocationOff, + contentDescription = stringResource(id = R.string.map), + modifier = + Modifier + .padding(30.dp) + .size(128.dp) + ) + Text( + stringResource(R.string.prominent_background_location_title), + textAlign = TextAlign.Center, + modifier = Modifier.padding(30.dp), + fontSize = 20.sp + ) + Text( + stringResource(R.string.prominent_background_location_message), + textAlign = TextAlign.Center, + modifier = Modifier.padding(30.dp), + fontSize = 15.sp + ) + Row( + modifier = + if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { + Modifier + .fillMaxWidth() + .padding(10.dp) + } else { + Modifier + .fillMaxWidth() + .padding(30.dp) + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly + ) { + TextButton(onClick = { + setLocationDisclosureShown() + }) { + Text(stringResource(id = R.string.no_thanks)) + } + TextButton(modifier = Modifier.focusRequester(focusRequester), onClick = { + openSettings() + setLocationDisclosureShown() + }) { + Text(stringResource(id = R.string.turn_on)) + } + } + } + return + } + + if (showAuthPrompt) { + AuthorizationPrompt( + onSuccess = { + showAuthPrompt = false + exportAllConfigs() + }, + onError = { error -> + showSnackbarMessage(error) + showAuthPrompt = false + }, + onFailure = { + showAuthPrompt = false + showSnackbarMessage(context.getString(R.string.authentication_failed)) + } + ) + } + + if (tunnels.isEmpty()) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = + Modifier + .fillMaxSize() + .padding(padding) + ) { + Text( + stringResource(R.string.one_tunnel_required), + textAlign = TextAlign.Center, + modifier = Modifier.padding(15.dp), + fontStyle = FontStyle.Italic + ) + } + return } - } - if(isLocationDisclaimerNeeded) { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top, - modifier = Modifier + modifier = + Modifier .fillMaxSize() .verticalScroll(scrollState) - .padding(padding) + .clickable(indication = null, interactionSource = interactionSource) { + focusManager.clearFocus() + } ) { - 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(context)) Modifier - .fillMaxWidth() - .padding(10.dp) else Modifier - .fillMaxWidth() - .padding(30.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceEvenly - ) { - TextButton(onClick = { - isLocationDisclaimerNeeded = false - }) { - Text(stringResource(id = R.string.no_thanks)) - } - TextButton(modifier = Modifier.focusRequester(focusRequester), onClick = { - openSettings() - }) { - Text(stringResource(id = R.string.turn_on)) - } - } - } - return - } - - - - if(showAuthPrompt) { - AuthorizationPrompt(onSuccess = { - showAuthPrompt = false - exportAllConfigs() }, - onError = { error -> - showSnackbarMessage(error) - showAuthPrompt = false - }, - onFailure = { - showAuthPrompt = false - showSnackbarMessage(context.getString(R.string.authentication_failed)) - }) - } - - if (tunnels.isEmpty()) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = Modifier - .fillMaxSize() - .padding(padding) - ) { - Text( - stringResource(R.string.one_tunnel_required), - textAlign = TextAlign.Center, - modifier = Modifier.padding(15.dp), - fontStyle = FontStyle.Italic - ) - } - return - } - - 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(context)) - Modifier - .height(IntrinsicSize.Min) - .fillMaxWidth(fillMaxWidth) - .padding(top = 10.dp) - else Modifier - .fillMaxWidth(fillMaxWidth) - .padding(top = 60.dp)).padding(bottom = 25.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 = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled), - checked = settings.isTunnelOnWifiEnabled, - padding = screenPadding, - onCheckChanged = { - scope.launch { - viewModel.onToggleTunnelOnWifi() - } - }, - modifier = Modifier.focusRequester(focusRequester) - ) - AnimatedVisibility(visible = settings.isTunnelOnWifiEnabled) { - Column { - FlowRow( - modifier = Modifier.padding(screenPadding), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.SpaceEvenly - ) { - trustedSSIDs.forEach { ssid -> - ClickableIconButton( - onIconClick = { - scope.launch { - viewModel.onDeleteTrustedSSID(ssid) - } - }, - text = ssid, - icon = Icons.Filled.Close, - enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled) - ) - } - if(trustedSSIDs.isEmpty()) { - Text(stringResource(R.string.none), fontStyle = FontStyle.Italic, color = Color.Gray) - } - } - OutlinedTextField( - enabled = !(settings.isAutoTunnelEnabled || 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) - .onFocusChanged { - if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { - keyboardController?.hide() - } - }, - maxLines = 1, - keyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.None, - imeAction = ImeAction.Done - ), - keyboardActions = KeyboardActions( - onDone = { - saveTrustedSSID() - } - ), - trailingIcon = { - 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 = if (currentText == "") Color.Transparent else MaterialTheme.colorScheme.primary - ) - } - }, - ) - } - } - ConfigurationToggle(stringResource(R.string.tunnel_mobile_data), - enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled), - checked = settings.isTunnelOnMobileDataEnabled, - padding = screenPadding, - onCheckChanged = { - scope.launch { - viewModel.onToggleTunnelOnMobileData() - } - } - ) - ConfigurationToggle(stringResource(id = R.string.tunnel_on_ethernet), - enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled), - checked = settings.isTunnelOnEthernetEnabled, - padding = screenPadding, - onCheckChanged = { - scope.launch { - viewModel.onToggleTunnelOnEthernet() - } - } - ) - ConfigurationToggle( - stringResource(R.string.battery_saver), - enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled), - checked = settings.isBatterySaverEnabled, - padding = screenPadding, - onCheckChanged = { - scope.launch { - viewModel.onToggleBatterySaver() - } - } - ) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxSize() - .padding(top = 5.dp), - horizontalArrangement = Arrangement.Center - ) { - TextButton( - enabled = !settings.isAlwaysOnVpnEnabled, - onClick = { - //TODO fix logic for mobile only - if(!isAllAutoTunnelPermissionsEnabled() && settings.isTunnelOnWifiEnabled) { - val message = if(!isBackgroundLocationGranted) { - context.getString(R.string.background_location_required) - } else if(viewModel.isLocationServicesNeeded()) { - context.getString(R.string.location_services_required) - } else { - context.getString(R.string.precise_location_required) - } - showSnackbarMessage(message) - } else scope.launch { - viewModel.toggleAutoTunnel() - } - }) { - val autoTunnelButtonText = if(settings.isAutoTunnelEnabled) stringResource(R.string.disable_auto_tunnel) - else stringResource(id = R.string.enable_auto_tunnel) - Text(autoTunnelButtonText) - } - } - } - } - if(!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { Surface( tonalElevation = 2.dp, shadowElevation = 2.dp, shape = RoundedCornerShape(12.dp), color = MaterialTheme.colorScheme.surface, - modifier = Modifier - .fillMaxWidth(fillMaxWidth) - .height(IntrinsicSize.Min) - .padding(bottom = 180.dp) + modifier = + ( + if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { + Modifier + .height(IntrinsicSize.Min) + .fillMaxWidth(fillMaxWidth) + .padding(top = 10.dp) + } else { + Modifier + .fillMaxWidth(fillMaxWidth) + .padding(top = 60.dp) + } + ).padding(bottom = 10.dp) ) { Column( horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.Top, modifier = Modifier.padding(15.dp) ) { - SectionTitle(title = stringResource(id = R.string.other), padding = screenPadding) - ConfigurationToggle(stringResource(R.string.always_on_vpn_support), - enabled = !settings.isAutoTunnelEnabled, - checked = settings.isAlwaysOnVpnEnabled, + SectionTitle( + title = stringResource(id = R.string.auto_tunneling), + padding = screenPadding + ) + ConfigurationToggle( + stringResource(id = R.string.tunnel_on_wifi), + enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled), + checked = settings.isTunnelOnWifiEnabled, padding = screenPadding, onCheckChanged = { scope.launch { - viewModel.onToggleAlwaysOnVPN() + viewModel.onToggleTunnelOnWifi() + } + }, + modifier = Modifier.focusRequester(focusRequester) + ) + AnimatedVisibility(visible = settings.isTunnelOnWifiEnabled) { + Column { + FlowRow( + modifier = Modifier + .padding(screenPadding) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(5.dp) + ) { + trustedSSIDs.forEach { ssid -> + ClickableIconButton( + onIconClick = { + scope.launch { + viewModel.onDeleteTrustedSSID(ssid) + } + }, + text = ssid, + icon = Icons.Filled.Close, + enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled) + ) + } + if (trustedSSIDs.isEmpty()) { + Text( + stringResource(R.string.none), + fontStyle = FontStyle.Italic, + color = Color.Gray + ) + } + } + OutlinedTextField( + enabled = !(settings.isAutoTunnelEnabled || 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) + .onFocusChanged { + if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { + keyboardController?.hide() + } + }, + 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 = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled), + checked = settings.isTunnelOnMobileDataEnabled, + padding = screenPadding, + onCheckChanged = { + scope.launch { + viewModel.onToggleTunnelOnMobileData() } } ) - ConfigurationToggle(stringResource(R.string.enabled_app_shortcuts), - enabled = true, - checked = settings.isShortcutsEnabled, + ConfigurationToggle( + stringResource(id = R.string.tunnel_on_ethernet), + enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled), + checked = settings.isTunnelOnEthernetEnabled, padding = screenPadding, onCheckChanged = { scope.launch { - viewModel.onToggleShortcutsEnabled() + viewModel.onToggleTunnelOnEthernet() + } + } + ) + ConfigurationToggle( + stringResource(R.string.battery_saver), + enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled), + checked = settings.isBatterySaverEnabled, + padding = screenPadding, + onCheckChanged = { + scope.launch { + viewModel.onToggleBatterySaver() } } ) Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier + modifier = + Modifier .fillMaxSize() .padding(top = 5.dp), horizontalArrangement = Arrangement.Center ) { TextButton( - enabled = !didExportFiles, + enabled = !settings.isAlwaysOnVpnEnabled, onClick = { - showAuthPrompt = true - }) { - Text(stringResource(R.string.export_configs)) + if (!isAllAutoTunnelPermissionsEnabled() && settings.isTunnelOnWifiEnabled) { + val message = + if (!isBackgroundLocationGranted) { + context.getString(R.string.background_location_required) + } else if (viewModel.isLocationServicesNeeded()) { + context.getString(R.string.location_services_required) + } else { + context.getString(R.string.precise_location_required) + } + showSnackbarMessage(message) + } else { + scope.launch { + viewModel.toggleAutoTunnel() + } + } + } + ) { + val autoTunnelButtonText = + if (settings.isAutoTunnelEnabled) { + stringResource(R.string.disable_auto_tunnel) + } else { + stringResource(id = R.string.enable_auto_tunnel) + } + Text(autoTunnelButtonText) } } } } - } - if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { - Spacer(modifier = Modifier.weight(.17f)) + if (WgQuickBackend.hasKernelSupport()) { + Surface( + tonalElevation = 2.dp, + shadowElevation = 2.dp, + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surface, + modifier = Modifier + .fillMaxWidth(fillMaxWidth) + .padding(vertical = 10.dp) + ) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier.padding(15.dp) + ) { + SectionTitle( + title = stringResource(id = R.string.kernel), + padding = screenPadding + ) + ConfigurationToggle( + stringResource(R.string.use_kernel), + enabled = !( + settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled || + (vpnState.value == Tunnel.State.UP) + ), + checked = settings.isKernelEnabled, + padding = screenPadding, + onCheckChanged = { + scope.launch { + try { + viewModel.onToggleKernelMode() + } catch (e: WgTunnelException) { + showSnackbarMessage(e.message) + } + } + } + ) + } + } + } + if (!WireGuardAutoTunnel.isRunningOnAndroidTv(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 + ) + ConfigurationToggle( + stringResource(R.string.always_on_vpn_support), + enabled = !settings.isAutoTunnelEnabled, + checked = settings.isAlwaysOnVpnEnabled, + padding = screenPadding, + onCheckChanged = { + scope.launch { + viewModel.onToggleAlwaysOnVPN() + } + } + ) + ConfigurationToggle( + stringResource(R.string.enabled_app_shortcuts), + enabled = true, + checked = settings.isShortcutsEnabled, + padding = screenPadding, + onCheckChanged = { + scope.launch { + viewModel.onToggleShortcutsEnabled() + } + } + ) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .fillMaxSize() + .padding(top = 5.dp), + horizontalArrangement = Arrangement.Center + ) { + TextButton( + enabled = !didExportFiles, + onClick = { + showAuthPrompt = true + } + ) { + Text(stringResource(R.string.export_configs)) + } + } + } + } + } + if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { + Spacer(modifier = Modifier.weight(.17f)) + } } } -} - - 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 f7c4672..959b9f6 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 @@ -6,32 +6,45 @@ import android.location.LocationManager import android.os.Build import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.wireguard.android.util.RootShell import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao +import com.zaneschepke.wireguardautotunnel.repository.datastore.DataStoreManager import com.zaneschepke.wireguardautotunnel.repository.model.Settings import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager +import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.util.WgTunnelException import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch -import javax.inject.Inject - +import timber.log.Timber @HiltViewModel -class SettingsViewModel @Inject constructor(private val application : Application, - private val tunnelRepo : TunnelConfigDao, private val settingsRepo : SettingsDoa +class SettingsViewModel +@Inject +constructor( + private val application: Application, + private val tunnelRepo: TunnelConfigDao, + private val settingsRepo: SettingsDoa, + val dataStoreManager: DataStoreManager, + private val rootShell: RootShell, + private val vpnService: VpnService ) : ViewModel() { private val _trustedSSIDs = MutableStateFlow(emptyList()) val trustedSSIDs = _trustedSSIDs.asStateFlow() private val _settings = MutableStateFlow(Settings()) val settings get() = _settings.asStateFlow() + val vpnState get() = vpnService.state val tunnels get() = tunnelRepo.getAllFlow() + val disclosureShown = dataStoreManager.locationDisclosureFlow + init { isLocationServicesEnabled() viewModelScope.launch(Dispatchers.IO) { @@ -42,7 +55,6 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio } } } - suspend fun onSaveTrustedSSID(ssid: String) { val trimmed = ssid.trim() if (!_settings.value.trustedNetworkSSIDs.contains(trimmed)) { @@ -54,9 +66,11 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio } suspend fun onToggleTunnelOnMobileData() { - settingsRepo.save(_settings.value.copy( - isTunnelOnMobileDataEnabled = !_settings.value.isTunnelOnMobileDataEnabled - )) + settingsRepo.save( + _settings.value.copy( + isTunnelOnMobileDataEnabled = !_settings.value.isTunnelOnMobileDataEnabled + ) + ) } suspend fun onDeleteTrustedSSID(ssid: String) { @@ -64,34 +78,40 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio settingsRepo.save(_settings.value) } - private fun emitFirstTunnelAsDefault() = viewModelScope.async { - _settings.emit(_settings.value.copy(defaultTunnel = getFirstTunnelConfig().toString())) - } + private fun emitFirstTunnelAsDefault() = + viewModelScope.async { + _settings.emit(_settings.value.copy(defaultTunnel = getFirstTunnelConfig().toString())) + } suspend fun toggleAutoTunnel() { - if(_settings.value.isAutoTunnelEnabled) { + if (_settings.value.isAutoTunnelEnabled) { ServiceManager.stopWatcherService(application) } else { - if(_settings.value.defaultTunnel == null) { + if (_settings.value.defaultTunnel == null) { emitFirstTunnelAsDefault().await() } val defaultTunnel = _settings.value.defaultTunnel ServiceManager.startWatcherService(application, defaultTunnel!!) } - settingsRepo.save(_settings.value.copy( - isAutoTunnelEnabled = !_settings.value.isAutoTunnelEnabled - )) + settingsRepo.save( + _settings.value.copy( + isAutoTunnelEnabled = !_settings.value.isAutoTunnelEnabled + ) + ) } - private suspend fun getFirstTunnelConfig() : TunnelConfig { + private suspend fun getFirstTunnelConfig(): TunnelConfig { return tunnelRepo.getAll().first() } suspend fun onToggleAlwaysOnVPN() { - if(_settings.value.defaultTunnel == null) { + if (_settings.value.defaultTunnel == null) { emitFirstTunnelAsDefault().await() } - val updatedSettings = _settings.value.copy(isAlwaysOnVpnEnabled = !_settings.value.isAlwaysOnVpnEnabled) + val updatedSettings = + _settings.value.copy( + isAlwaysOnVpnEnabled = !_settings.value.isAlwaysOnVpnEnabled + ) emitSettings(updatedSettings) saveSettings(updatedSettings) } @@ -107,40 +127,71 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio } suspend fun onToggleTunnelOnEthernet() { - if(_settings.value.defaultTunnel == null) { + if (_settings.value.defaultTunnel == null) { emitFirstTunnelAsDefault().await() } _settings.emit( - _settings.value.copy(isTunnelOnEthernetEnabled = !_settings.value.isTunnelOnEthernetEnabled) + _settings.value.copy( + isTunnelOnEthernetEnabled = !_settings.value.isTunnelOnEthernetEnabled + ) ) settingsRepo.save(_settings.value) } - private fun isLocationServicesEnabled() : Boolean { + private fun isLocationServicesEnabled(): Boolean { val locationManager = application.getSystemService(Context.LOCATION_SERVICE) as LocationManager return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) } - fun isLocationServicesNeeded() : Boolean { - return(!isLocationServicesEnabled() && Build.VERSION.SDK_INT > Build.VERSION_CODES.P) + fun isLocationServicesNeeded(): Boolean { + return (!isLocationServicesEnabled() && Build.VERSION.SDK_INT > Build.VERSION_CODES.P) } suspend fun onToggleShortcutsEnabled() { - settingsRepo.save(_settings.value.copy( - isShortcutsEnabled = !_settings.value.isShortcutsEnabled - )) + settingsRepo.save( + _settings.value.copy( + isShortcutsEnabled = !_settings.value.isShortcutsEnabled + ) + ) } suspend fun onToggleBatterySaver() { - settingsRepo.save(_settings.value.copy( - isBatterySaverEnabled = !_settings.value.isBatterySaverEnabled - )) + settingsRepo.save( + _settings.value.copy( + isBatterySaverEnabled = !_settings.value.isBatterySaverEnabled + ) + ) + } + + private suspend fun saveKernelMode(on: Boolean) { + settingsRepo.save( + _settings.value.copy( + isKernelEnabled = on + ) + ) + } + + suspend fun onToggleKernelMode() { + if (!_settings.value.isKernelEnabled) { + try { + rootShell.start() + Timber.d("Root shell accepted!") + saveKernelMode(on = true) + } catch (e: RootShell.RootShellException) { + saveKernelMode(on = false) + throw WgTunnelException("Root shell denied!") + } + } else { + saveKernelMode(on = false) + } } suspend fun onToggleTunnelOnWifi() { - settingsRepo.save(_settings.value.copy( - isTunnelOnWifiEnabled = !_settings.value.isTunnelOnWifiEnabled - )) + settingsRepo.save( + _settings.value.copy( + isTunnelOnWifiEnabled = !_settings.value.isTunnelOnWifiEnabled + ) + ) } -} \ No newline at end of file +} 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 58a6eed..9ac9c64 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 @@ -30,6 +30,7 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -44,105 +45,200 @@ import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.content.ContextCompat.startActivity +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.zaneschepke.wireguardautotunnel.BuildConfig import com.zaneschepke.wireguardautotunnel.Constants import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel +import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsViewModel @Composable -fun SupportScreen(padding : PaddingValues, focusRequester: FocusRequester) { - +fun SupportScreen( + viewModel: SettingsViewModel = hiltViewModel(), + padding: PaddingValues, + focusRequester: FocusRequester +) { val context = LocalContext.current val fillMaxWidth = .85f + val settings by viewModel.settings.collectAsStateWithLifecycle() + fun openWebPage(url: String) { val webpage: Uri = Uri.parse(url) val intent = Intent(Intent.ACTION_VIEW, webpage) context.startActivity(intent) } - + fun launchEmail() { - val intent = Intent(Intent.ACTION_SEND).apply { - type = Constants.EMAIL_MIME_TYPE - putExtra(Intent.EXTRA_EMAIL, context.getString(R.string.my_email)) - putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject)) - } - startActivity(context,createChooser(intent, context.getString(R.string.email_chooser)),null) + val intent = + Intent(Intent.ACTION_SEND).apply { + type = Constants.EMAIL_MIME_TYPE + putExtra(Intent.EXTRA_EMAIL, context.getString(R.string.my_email)) + putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject)) + } + startActivity( + context, + createChooser(intent, context.getString(R.string.email_chooser)), + null + ) } - Column(horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top, - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .focusable() - .padding(padding)) { - Surface( - tonalElevation = 2.dp, - shadowElevation = 2.dp, - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surface, - modifier = (if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) - 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)) { - Text(stringResource(R.string.thank_you), textAlign = TextAlign.Start, modifier = Modifier.padding(bottom = 20.dp), fontSize = 16.sp) - Text(stringResource(id = R.string.support_help_text), textAlign = TextAlign.Start, fontSize = 16.sp, modifier = Modifier.padding(bottom = 20.dp)) - TextButton(onClick = { openWebPage(context.resources.getString(R.string.docs_url)) }, modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester)) { - Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { - Row { - Icon(Icons.Rounded.Book, stringResource(id = R.string.docs)) - Text(stringResource(id = R.string.docs_description), textAlign = TextAlign.Justify, modifier = Modifier.padding(start = 10.dp)) - } - Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go)) - } + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + modifier = + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .focusable() + .padding(padding) + ) { + Surface( + tonalElevation = 2.dp, + shadowElevation = 2.dp, + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surface, + modifier = + ( + if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { + Modifier + .height(IntrinsicSize.Min) + .fillMaxWidth(fillMaxWidth) + .padding(top = 10.dp) + } else { + Modifier + .fillMaxWidth(fillMaxWidth) + .padding(top = 20.dp) } - Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp) - TextButton(onClick = { openWebPage(context.resources.getString(R.string.discord_url)) }, modifier = Modifier.padding(vertical = 5.dp)) { - Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { - Row { - Icon(imageVector = ImageVector.vectorResource(R.drawable.discord), stringResource( - id = R.string.discord), Modifier.size(25.dp)) - Text(stringResource(id = R.string.discord_description), textAlign = TextAlign.Justify, modifier = Modifier.padding(start = 10.dp)) - } - Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go)) + ).padding(bottom = 25.dp) + ) { + Column(modifier = Modifier.padding(20.dp)) { + Text( + stringResource(R.string.thank_you), + textAlign = TextAlign.Start, + modifier = Modifier.padding(bottom = 20.dp), + fontSize = 16.sp + ) + Text( + stringResource(id = R.string.support_help_text), + textAlign = TextAlign.Start, + fontSize = 16.sp, + modifier = Modifier.padding(bottom = 20.dp) + ) + TextButton(onClick = { + openWebPage(context.resources.getString(R.string.docs_url)) + }, modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester)) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Row { + Icon(Icons.Rounded.Book, stringResource(id = R.string.docs)) + Text( + stringResource(id = R.string.docs_description), + textAlign = TextAlign.Justify, + modifier = Modifier.padding(start = 10.dp) + ) } + Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go)) } - Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp) - TextButton(onClick = { openWebPage(context.resources.getString(R.string.github_url)) }, modifier = Modifier.padding(vertical = 5.dp)) { - Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { - Row { - Icon(imageVector = ImageVector.vectorResource(R.drawable.github), stringResource( + } + Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp) + TextButton( + onClick = { openWebPage(context.resources.getString(R.string.discord_url)) }, + modifier = Modifier.padding(vertical = 5.dp) + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Row { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.discord), + stringResource( + id = R.string.discord + ), + Modifier.size(25.dp) + ) + Text( + stringResource(id = R.string.discord_description), + textAlign = TextAlign.Justify, + modifier = Modifier.padding(start = 10.dp) + ) + } + Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go)) + } + } + Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp) + TextButton( + onClick = { openWebPage(context.resources.getString(R.string.github_url)) }, + modifier = Modifier.padding(vertical = 5.dp) + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Row { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.github), + stringResource( id = R.string.github - ), Modifier.size(25.dp)) - Text("Open an issue", textAlign = TextAlign.Justify, modifier = Modifier.padding(start = 10.dp)) - } - Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go)) + ), + Modifier.size(25.dp) + ) + Text( + "Open an issue", + textAlign = TextAlign.Justify, + modifier = Modifier.padding(start = 10.dp) + ) } + Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go)) } - Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp) - TextButton(onClick = { launchEmail() }, modifier = Modifier.padding(vertical = 5.dp)) { - Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { - Row { - Icon(Icons.Rounded.Mail, stringResource(id = R.string.email)) - Text(stringResource(id = R.string.email_description), textAlign = TextAlign.Justify, modifier = Modifier.padding(start = 10.dp)) - } - Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go)) + } + Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp) + TextButton( + onClick = { launchEmail() }, + modifier = Modifier.padding(vertical = 5.dp) + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Row { + Icon(Icons.Rounded.Mail, stringResource(id = R.string.email)) + Text( + stringResource(id = R.string.email_description), + textAlign = TextAlign.Justify, + modifier = Modifier.padding(start = 10.dp) + ) } + Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go)) } } } - Spacer(modifier = Modifier.weight(1f)) - Text(stringResource(id = R.string.privacy_policy), style = TextStyle(textDecoration = TextDecoration.Underline), - modifier = Modifier.clickable { - openWebPage(context.resources.getString(R.string.privacy_policy_url)) - }) - Text("App version: ${BuildConfig.VERSION_NAME}", Modifier.padding(25.dp)) } - } \ No newline at end of file + Spacer(modifier = Modifier.weight(1f)) + Text( + stringResource(id = R.string.privacy_policy), + style = TextStyle(textDecoration = TextDecoration.Underline), + fontSize = 16.sp, + modifier = + Modifier.clickable { + openWebPage(context.resources.getString(R.string.privacy_policy_url)) + } + ) + Row( + horizontalArrangement = Arrangement.spacedBy(25.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(25.dp) + ) { + Text("Version: ${BuildConfig.VERSION_NAME}") + Text("Mode: ${if (settings.isKernelEnabled) "Kernel" else "Userspace" }") + } + } +} 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 new file mode 100644 index 0000000..b58ac11 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportViewModel.kt @@ -0,0 +1,25 @@ +package com.zaneschepke.wireguardautotunnel.ui.screens.support + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa +import com.zaneschepke.wireguardautotunnel.repository.model.Settings +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +@HiltViewModel +class SupportViewModel @Inject constructor( + private val settingsRepo: SettingsDoa +) : ViewModel() { + private val _settings = MutableStateFlow(Settings()) + val settings get() = _settings.asStateFlow() + init { + viewModelScope.launch(Dispatchers.IO) { + _settings.value = settingsRepo.getAll().first() + } + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Color.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Color.kt index b886444..d5a2e23 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Color.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Color.kt @@ -11,7 +11,8 @@ val Purple40 = Color(0xFF6650a4) val PurpleGrey40 = Color(0xFF625b71) val Pink40 = Color(0xFFFFFFFF) -//status colors +// status colors val brickRed = Color(0xFFCE4257) +val corn = Color(0xFFFBEC5D) val pinkRed = Color(0xFFEF476F) val mint = Color(0xFF52B788) 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 78e4480..d4d529b 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 @@ -15,51 +15,52 @@ import androidx.compose.ui.platform.LocalContext 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 -) +private val DarkColorScheme = + darkColorScheme( + // primary = Purple80, + primary = virdigris, + secondary = virdigris, + // secondary = PurpleGrey80, + tertiary = virdigris + // tertiary = Pink80 + ) -private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40 - - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ -) +private val LightColorScheme = + lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ + ) @Composable fun WireguardAutoTunnelTheme( - //force dark theme - darkTheme : Boolean = true, - //darkTheme: Boolean = isSystemInDarkTheme(), + // force dark theme + darkTheme: Boolean = true, + // darkTheme: Boolean = isSystemInDarkTheme(), // Dynamic color is available on Android 12+ - //turning off dynamic color for now + // 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 } - - darkTheme -> DarkColorScheme - else -> LightColorScheme - } val view = LocalView.current if (!view.isInEditMode) { SideEffect { @@ -77,4 +78,4 @@ fun WireguardAutoTunnelTheme( typography = Typography, content = content ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/TransparentSystemBars.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/TransparentSystemBars.kt index 64242c6..62ee01f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/TransparentSystemBars.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/TransparentSystemBars.kt @@ -19,4 +19,4 @@ fun TransparentSystemBars() { onDispose {} } -} \ No newline at end of file +} 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 e7a1e29..9f0e579 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 @@ -7,28 +7,30 @@ import androidx.compose.ui.text.font.FontWeight 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 +val Typography = + 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, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ ) - /* Other default text styles to override - titleLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - labelSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) - */ -) \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/StorageUtil.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/FileUtils.kt similarity index 54% rename from app/src/main/java/com/zaneschepke/wireguardautotunnel/util/StorageUtil.kt rename to app/src/main/java/com/zaneschepke/wireguardautotunnel/util/FileUtils.kt index f01e3cc..b86e565 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/StorageUtil.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/FileUtils.kt @@ -13,36 +13,49 @@ import java.time.Instant import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream -object StorageUtil { +object FileUtils { private const val ZIP_FILE_MIME_TYPE = "application/zip" - private fun createDownloadsFileOutputStream(context: Context, fileName: String, mimeType : String = Constants.ALLOWED_FILE_TYPES) : OutputStream? { + + private fun createDownloadsFileOutputStream( + context: Context, + 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 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 - ) + val target = + File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), + fileName + ) return target.outputStream() } return null } - fun saveFilesToZip(context: Context, files : List) { - val zipOutputStream = createDownloadsFileOutputStream(context, "wg-export_${Instant.now().epochSecond}.zip", ZIP_FILE_MIME_TYPE) + fun saveFilesToZip( + context: Context, + files: List + ) { + val zipOutputStream = createDownloadsFileOutputStream( + context, + "wg-export_${Instant.now().epochSecond}.zip", + ZIP_FILE_MIME_TYPE + ) ZipOutputStream(zipOutputStream).use { zos -> files.forEach { file -> - val entry = ZipEntry( file.name) + val entry = ZipEntry(file.name) zos.putNextEntry(entry) if (file.isFile) { file.inputStream().use { fis -> fis.copyTo(zos) } @@ -50,4 +63,4 @@ object StorageUtil { } } } -} \ No newline at end of file +} 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 2aec795..50af9eb 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/NumberUtils.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/NumberUtils.kt @@ -6,24 +6,23 @@ 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() - fun bytesToMB(bytes : Long) : BigDecimal { + fun bytesToMB(bytes: Long): BigDecimal { return bytes.toBigDecimal().divide(BYTES_IN_MB.toBigDecimal()) } - fun isValidKey(key : String) : Boolean { + fun isValidKey(key: String): Boolean { return key.matches(keyValidationRegex) } - fun generateRandomTunnelName() : String { + fun generateRandomTunnelName(): String { return "tunnel${(Math.random() * 100000).toInt()}" } - fun getSecondsBetweenTimestampAndNow(epoch : Long) : Long? { + fun getSecondsBetweenTimestampAndNow(epoch: Long): Long? { return if (epoch != 0L) { val time = Instant.ofEpochMilli(epoch) return Duration.between(time, Instant.now()).seconds @@ -31,4 +30,4 @@ object NumberUtils { null } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/WgTunnelException.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/WgTunnelException.kt index d73b810..544bf5a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/WgTunnelException.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/WgTunnelException.kt @@ -3,13 +3,14 @@ package com.zaneschepke.wireguardautotunnel.util import com.wireguard.config.BadConfigException class WgTunnelException(e: Exception) : Exception() { - constructor(message : String) : this(Exception(message)) + constructor(message: String) : this(Exception(message)) override val message: String = generateExceptionMessage(e) - private fun generateExceptionMessage(e : Exception) : String { - return when(e) { + + private fun generateExceptionMessage(e: Exception): String { + return when (e) { is BadConfigException -> "${e.section.name} ${e.location.name} ${e.reason.name}" else -> e.message ?: "Unknown error occurred" } } -} \ No newline at end of file +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5c19c08..80f453e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -152,5 +152,6 @@ Email Send me an email If you are experiencing issues, have improvement ideas, or just want to engage, the following resources are available: - + Kernel + Use kernel module \ No newline at end of file diff --git a/app/src/test/java/com/zaneschepke/wireguardautotunnel/ExampleUnitTest.kt b/app/src/test/java/com/zaneschepke/wireguardautotunnel/ExampleUnitTest.kt index 8730d6a..435f6ba 100644 --- a/app/src/test/java/com/zaneschepke/wireguardautotunnel/ExampleUnitTest.kt +++ b/app/src/test/java/com/zaneschepke/wireguardautotunnel/ExampleUnitTest.kt @@ -13,4 +13,4 @@ class ExampleUnitTest { fun addition_isCorrect() { assertEquals(4, 2 + 2) } -} \ No newline at end of file +} diff --git a/build.gradle.kts b/build.gradle.kts index 03881b7..20ac053 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,5 +12,5 @@ plugins { alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.hilt.android) apply false kotlin("plugin.serialization").version(libs.versions.kotlin).apply(false) - alias(libs.plugins.ksp) apply false + alias(libs.plugins.ksp) apply false } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 37871be..1a4f1f4 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -5,4 +5,4 @@ plugins { repositories { google() mavenCentral() -} \ No newline at end of file +} diff --git a/buildSrc/src/main/kotlin/BuildHelper.kt b/buildSrc/src/main/kotlin/BuildHelper.kt index 13b6bd4..8d413c5 100644 --- a/buildSrc/src/main/kotlin/BuildHelper.kt +++ b/buildSrc/src/main/kotlin/BuildHelper.kt @@ -1,29 +1,36 @@ import org.gradle.api.invocation.Gradle object BuildHelper { - private fun getCurrentFlavor(gradle : Gradle): String { + private fun getCurrentFlavor(gradle: Gradle): String { val taskRequestsStr = gradle.startParameter.taskRequests.toString() - val pattern: java.util.regex.Pattern = if (taskRequestsStr.contains("assemble")) { - java.util.regex.Pattern.compile("assemble(\\w+)(Release|Debug)") - } else { - java.util.regex.Pattern.compile("bundle(\\w+)(Release|Debug)") - } + val pattern: java.util.regex.Pattern = + if (taskRequestsStr.contains("assemble")) { + java.util.regex.Pattern.compile("assemble(\\w+)(Release|Debug)") + } else { + java.util.regex.Pattern.compile("bundle(\\w+)(Release|Debug)") + } val matcher = pattern.matcher(taskRequestsStr) - val flavor = if (matcher.find()) { - matcher.group(1).lowercase() - } else { - print("NO FLAVOR FOUND") - "" - } + val flavor = + if (matcher.find()) { + matcher.group(1).lowercase() + } else { + print("NO FLAVOR FOUND") + "" + } return flavor } - fun isGeneralFlavor(gradle : Gradle) : Boolean { + fun isGeneralFlavor(gradle: Gradle): Boolean { return getCurrentFlavor(gradle) == "general" } - fun isReleaseBuild(gradle: Gradle) : Boolean { - return (gradle.startParameter.taskNames.size > 0 && gradle.startParameter.taskNames[0].contains( - "Release")) + + fun isReleaseBuild(gradle: Gradle): Boolean { + return ( + gradle.startParameter.taskNames.size > 0 && + gradle.startParameter.taskNames[0].contains( + "Release", + ) + ) } -} \ No newline at end of file +} diff --git a/buildSrc/src/main/kotlin/Constants.kt b/buildSrc/src/main/kotlin/Constants.kt index c2f75e9..dda8288 100644 --- a/buildSrc/src/main/kotlin/Constants.kt +++ b/buildSrc/src/main/kotlin/Constants.kt @@ -1,7 +1,7 @@ object Constants { - const val VERSION_NAME = "3.2.3" + const val VERSION_NAME = "3.2.4" const val JVM_TARGET = "17" - const val VERSION_CODE = 32300 + const val VERSION_CODE = 32400 const val TARGET_SDK = 34 const val MIN_SDK = 26 const val APP_ID = "com.zaneschepke.wireguardautotunnel" @@ -14,4 +14,4 @@ object Constants { const val RELEASE = "release" const val TYPE = "type" -} \ No newline at end of file +} diff --git a/fastlane/metadata/android/en-US/changelogs/32400.txt b/fastlane/metadata/android/en-US/changelogs/32400.txt new file mode 100644 index 0000000..13b8084 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/32400.txt @@ -0,0 +1,5 @@ +Enhancements: +- Add basic WireGuard Kernel support +- Improved location disclosure flow +- Fix auto-tunnel permissions bug +- Various other UI bug fixes \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index acb883d..c21f0b3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,11 +1,12 @@ [versions] accompanist = "0.32.0" -activityCompose = "1.8.1" +activityCompose = "1.8.2" androidx-junit = "1.1.5" appcompat = "1.6.1" biometricKtx = "1.2.0-alpha05" coreGoogleShortcuts = "1.1.0" coreKtx = "1.12.0" +datastorePreferences = "1.0.0" desugar_jdk_libs = "2.0.4" espressoCore = "3.5.1" firebase-crashlytics-gradle = "2.9.9" @@ -17,7 +18,7 @@ kotlinx-serialization-json = "1.6.2" lifecycle-runtime-compose = "2.6.2" material-icons-extended = "1.5.4" material3 = "1.1.2" -navigationCompose = "2.7.5" +navigationCompose = "2.7.6" roomVersion = "2.6.1" timber = "5.0.1" tunnel = "1.0.20230706" @@ -25,7 +26,7 @@ androidGradlePlugin = "8.2.0" kotlin="1.9.10" ksp="1.9.10-1.0.13" composeBom="2023.10.01" -firebaseBom= "32.6.0" +firebaseBom= "32.7.0" compose="1.5.4" crashlytics= "18.6.0" analytics="21.5.0" @@ -45,6 +46,7 @@ accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist- androidx-biometric-ktx = { module = "androidx.biometric:biometric-ktx", version.ref = "biometricKtx" } androidx-core = { module = "androidx.core:core", version.ref = "coreKtx" } androidx-core-google-shortcuts = { module = "androidx.core:core-google-shortcuts", version.ref = "coreGoogleShortcuts" } +androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle-runtime-compose" } androidx-lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", version.ref = "lifecycle-runtime-compose" } androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomVersion" } @@ -61,6 +63,7 @@ androidx-compose-ui-tooling-preview = { module="androidx.compose.ui:ui-tooling-p androidx-compose-ui = { module="androidx.compose.ui:ui", version.ref="compose" } #hilt +androidx-room-testing = { module = "androidx.room:room-testing", version.ref = "roomVersion" } desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" } hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroid" } diff --git a/index.html b/index.html deleted file mode 100644 index e00722a..0000000 --- a/index.html +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - Document - - - -Privacy Policy -============== - -WG Tunnel provides an alternative Android client app for network tunnels using the WireGuard Protocol. - -Information you provide ------------------------ - -No information provided to the App is transmitted to me or anyone else. -The App does not collect information for purposes of our collection. Your -data is not collected. - -Background Location ------------------------- - -This application does collect location information (specifically Wi-Fi ssid name) in the background -for the auto tunnel feature. This information is not stored or transmitted but is simple collected -by the app to determine whether or not to turn on the VPN. - -Updates to this document ------------------------- - -I will keep this document up-to-date. Your continued use of WG Tunnel confirms -your acceptance of this Privacy Policy. - -Contact Me ----------- - -If you have questions about this Privacy Policy, please contact me -zanecschepke@gmail.com or Discord (invite link on this repository). - - -Effective as of May 24, 2023 -Updated May 24, 2023 - - - diff --git a/settings.gradle.kts b/settings.gradle.kts index 6844a38..308d6d9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,4 +15,3 @@ dependencyResolutionManagement { rootProject.name = "WG Tunnel" include(":app") - \ No newline at end of file