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