From 54d9653f04aafdbed113d20c913f061b51987afc Mon Sep 17 00:00:00 2001 From: Zane Schepke Date: Thu, 30 May 2024 23:10:28 -0400 Subject: [PATCH] fix: mobile data tunneling Fixes a bug where mobile data tunneling was not working properly in certain scenarios. Fixes an issue where the new floating action button was not working correctly on AndroidTV. Improved local logging. Additional refactors and optimizations. --- .github/CODE_OF_CONDUCT.md | 2 +- .github/workflows/issue-workflow.yml | 2 +- .github/workflows/publish-workflow.yml | 2 +- README.md | 16 +- app/build.gradle.kts | 1 - app/fdroid-rules.pro | 2 +- app/proguard-rules.pro | 4 +- .../WireGuardAutoTunnel.kt | 48 ++- .../wireguardautotunnel/data/AppDatabase.kt | 2 +- .../data/TunnelConfigDao.kt | 6 +- .../data/datastore/DataStoreManager.kt | 44 ++- .../data/domain/TunnelConfig.kt | 4 +- .../data/repository/TunnelConfigRepository.kt | 2 +- .../wireguardautotunnel/module/AppModule.kt | 30 ++ .../{Userspace.kt => BackendQualifiers.kt} | 4 + .../module/CoroutineQualifiers.kt | 27 ++ .../module/CoroutinesDispatchersModule.kt | 28 ++ .../module/DatabaseModule.kt | 30 -- .../wireguardautotunnel/module/Kernel.kt | 7 - .../module/RepositoryModule.kt | 24 +- .../module/TunnelModule.kt | 24 +- .../module/ViewModelModule.kt | 25 ++ .../service/foreground/ForegroundService.kt | 1 + .../service/foreground/ServiceManager.kt | 61 +-- .../service/foreground/WatcherState.kt | 7 - .../WireGuardConnectivityWatcherService.kt | 364 ++++++++++-------- .../foreground/WireGuardTunnelService.kt | 77 ++-- .../service/shortcut/ShortcutsActivity.kt | 10 +- .../service/tile/AutoTunnelControlTile.kt | 48 +-- .../service/tile/TunnelControlTile.kt | 25 +- .../service/tunnel/TunnelState.kt | 17 +- .../service/tunnel/WireGuardTunnel.kt | 96 ++--- .../tunnel/statistics/AmneziaStatistics.kt | 2 +- .../tunnel/statistics/TunnelStatistics.kt | 8 +- .../tunnel/statistics/WireGuardStatistics.kt | 4 +- .../wireguardautotunnel/ui/AppViewModel.kt | 44 +-- .../wireguardautotunnel/ui/MainActivity.kt | 36 +- .../ui/common/RowListItem.kt | 4 +- .../ui/common/config/ConfigurationToggle.kt | 15 +- .../ui/screens/config/ConfigScreen.kt | 38 +- .../ui/screens/config/ConfigUiState.kt | 5 +- .../ui/screens/config/ConfigViewModel.kt | 100 +++-- .../ui/screens/config/model/InterfaceProxy.kt | 30 +- .../ui/screens/config/model/PeerProxy.kt | 2 +- .../ui/screens/main/MainScreen.kt | 132 ++++--- .../ui/screens/main/MainViewModel.kt | 250 +++++++----- .../ui/screens/options/OptionsScreen.kt | 126 +++--- .../ui/screens/options/OptionsViewModel.kt | 11 +- .../ui/screens/settings/SettingsScreen.kt | 38 +- .../ui/screens/settings/SettingsViewModel.kt | 13 +- .../ui/screens/support/SupportScreen.kt | 30 +- .../ui/screens/support/logs/LogsScreen.kt | 29 +- .../ui/screens/support/logs/LogsViewModel.kt | 55 +++ .../wireguardautotunnel/util/Constants.kt | 3 +- .../wireguardautotunnel/util/Extensions.kt | 94 ++++- .../wireguardautotunnel/util/FileUtils.kt | 137 ++++--- .../util/WgTunnelExceptions.kt | 128 +++--- app/src/main/res/drawable/add.xml | 6 +- app/src/main/res/drawable/close.xml | 6 +- app/src/main/res/drawable/edit.xml | 6 +- app/src/main/res/drawable/telegram.xml | 6 +- buildSrc/src/main/kotlin/Constants.kt | 4 +- .../android/en-US/changelogs/34500.txt | 5 + gradle/libs.versions.toml | 6 +- gradle/wrapper/gradle-wrapper.properties | 2 +- logcatter/build.gradle.kts | 3 + .../logcatter/LocalLogCollector.kt | 13 + .../com/zaneschepke/logcatter/Logcatter.kt | 273 ++++++++++++- .../zaneschepke/logcatter/model/LogMessage.kt | 27 +- 69 files changed, 1764 insertions(+), 967 deletions(-) create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/module/AppModule.kt rename app/src/main/java/com/zaneschepke/wireguardautotunnel/module/{Userspace.kt => BackendQualifiers.kt} (68%) create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/module/CoroutineQualifiers.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/module/CoroutinesDispatchersModule.kt delete mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/module/DatabaseModule.kt delete mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/module/Kernel.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ViewModelModule.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsViewModel.kt create mode 100644 fastlane/metadata/android/en-US/changelogs/34500.txt create mode 100644 logcatter/src/main/java/com/zaneschepke/logcatter/LocalLogCollector.kt diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index bc3764f..e10b7cd 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -4,7 +4,7 @@ We as individuals involved in this project, pledge to participate in this community in a respectful, constructive, and civil manner as we work towards a common goal -of delivering free, open source, and value adding software for all. +of delivering free, open source, and value adding software for all. ## Standard diff --git a/.github/workflows/issue-workflow.yml b/.github/workflows/issue-workflow.yml index 172fc1f..5edc689 100644 --- a/.github/workflows/issue-workflow.yml +++ b/.github/workflows/issue-workflow.yml @@ -2,7 +2,7 @@ name: Issue Updates Workflow on: issues: - types: [opened, closed, reopened] + types: [ opened, closed, reopened ] jobs: diff --git a/.github/workflows/publish-workflow.yml b/.github/workflows/publish-workflow.yml index 126bde0..1fcb05c 100644 --- a/.github/workflows/publish-workflow.yml +++ b/.github/workflows/publish-workflow.yml @@ -2,7 +2,7 @@ name: Release Updates Workflow on: release: - types: [published] + types: [ published ] jobs: diff --git a/README.md b/README.md index e574046..3cf9749 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,8 @@ WG Tunnel
-This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) with added +This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) +and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) with added features. Built using the [wireguard-android](https://github.com/WireGuard/wireguard-android) library and [Jetpack Compose](https://developer.android.com/jetpack/compose), this application was inspired by the official [WireGuard Android](https://github.com/WireGuard/wireguard-android) app. @@ -73,18 +74,21 @@ The repository for these docs can be found [here](https://github.com/zaneschepke ## Contributing -Any contributions in the form of feedback, issues, code, or translations are welcome and much appreciated! +Any contributions in the form of feedback, issues, code, or translations are welcome and much +appreciated! -Please read the [code of conduct](https://github.com/zaneschepke/wgtunnel?tab=coc-ov-file#contributor-code-of-conduct) before contributing. +Please read +the [code of conduct](https://github.com/zaneschepke/wgtunnel?tab=coc-ov-file#contributor-code-of-conduct) +before contributing. ## Translation -This app is using [Weblate](https://weblate.org) to assist with translations. +This app is using [Weblate](https://weblate.org) to assist with translations. -Help translate WG Tunnel into your language at [Hosted Weblate](https://hosted.weblate.org/engage/wg-tunnel/).\ +Help translate WG Tunnel into your language +at [Hosted Weblate](https://hosted.weblate.org/engage/wg-tunnel/).\ [![Translation status](https://hosted.weblate.org/widgets/wg-tunnel/-/multi-auto.svg)](https://hosted.weblate.org/engage/wg-tunnel/) - ## Building ``` diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 46d7a3b..b4a4b1b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -203,7 +203,6 @@ dependencies { // barcode scanning implementation(libs.zxing.android.embedded) - implementation(libs.zxing.core) // bio implementation(libs.androidx.biometric.ktx) diff --git a/app/fdroid-rules.pro b/app/fdroid-rules.pro index 835738c..86f6534 100644 --- a/app/fdroid-rules.pro +++ b/app/fdroid-rules.pro @@ -2,4 +2,4 @@ -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 bb7a397..ebbadab 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -21,6 +21,4 @@ #-renamesourcefileattribute SourceFile -keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite { ; -} - - +} \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt index 7a67413..c271337 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt @@ -3,35 +3,59 @@ package com.zaneschepke.wireguardautotunnel import android.app.Application import android.content.ComponentName import android.content.pm.PackageManager +import android.os.StrictMode +import android.os.StrictMode.ThreadPolicy import android.service.quicksettings.TileService +import com.zaneschepke.logcatter.LocalLogCollector +import com.zaneschepke.wireguardautotunnel.module.ApplicationScope +import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile import com.zaneschepke.wireguardautotunnel.util.ReleaseTree import dagger.hilt.android.HiltAndroidApp -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.cancel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import timber.log.Timber import xyz.teamgravity.pin_lock_compose.PinManager +import javax.inject.Inject @HiltAndroidApp class WireGuardAutoTunnel : Application() { + + @Inject + lateinit var localLogCollector: LocalLogCollector + + @Inject + @ApplicationScope + lateinit var applicationScope: CoroutineScope + + @Inject + @IoDispatcher + lateinit var ioDispatcher: CoroutineDispatcher + override fun onCreate() { super.onCreate() instance = this - if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) else Timber.plant(ReleaseTree()) - PinManager.initialize(this) - } - - override fun onLowMemory() { - super.onLowMemory() - applicationScope.cancel("onLowMemory() called by system") - applicationScope = MainScope() + if (BuildConfig.DEBUG) { + Timber.plant(Timber.DebugTree()) + StrictMode.setThreadPolicy( + ThreadPolicy.Builder() + .detectDiskReads() + .detectDiskWrites() + .detectNetwork() + .penaltyLog() + .build(), + ) + } else Timber.plant(ReleaseTree()) + applicationScope.launch(ioDispatcher) { + PinManager.initialize(this@WireGuardAutoTunnel) + if (!isRunningOnAndroidTv()) localLogCollector.start() + } } companion object { - var applicationScope = MainScope() - lateinit var instance: WireGuardAutoTunnel private set diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt index b9e73f7..75e9fe3 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt @@ -33,7 +33,7 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig to = 7, spec = RemoveLegacySettingColumnsMigration::class, ), - AutoMigration(7, 8) + AutoMigration(7, 8), ], exportSchema = true, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/TunnelConfigDao.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/TunnelConfigDao.kt index d33e61e..1946c74 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/TunnelConfigDao.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/TunnelConfigDao.kt @@ -21,7 +21,7 @@ interface TunnelConfigDao { suspend fun getById(id: Long): TunnelConfig? @Query("SELECT * FROM TunnelConfig WHERE name=:name") - suspend fun getByName(name: String) : TunnelConfig? + suspend fun getByName(name: String): TunnelConfig? @Query("SELECT * FROM TunnelConfig") suspend fun getAll(): TunnelConfigs @@ -36,10 +36,10 @@ interface TunnelConfigDao { suspend fun findByTunnelNetworkName(name: String): TunnelConfigs @Query("UPDATE TunnelConfig SET is_primary_tunnel = 0 WHERE is_primary_tunnel =1") - fun resetPrimaryTunnel() + suspend fun resetPrimaryTunnel() @Query("UPDATE TunnelConfig SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1") - fun resetMobileDataTunnel() + suspend fun resetMobileDataTunnel() @Query("SELECT * FROM TUNNELCONFIG WHERE is_primary_tunnel=1") suspend fun findByPrimary(): TunnelConfigs diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/DataStoreManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/DataStoreManager.kt index eb8725c..befed3b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/DataStoreManager.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/DataStoreManager.kt @@ -7,14 +7,20 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore +import com.zaneschepke.wireguardautotunnel.module.IoDispatcher +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import timber.log.Timber import java.io.IOException -class DataStoreManager(private val context: Context) { +class DataStoreManager( + private val context: Context, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher +) { companion object { val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN") val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN") @@ -32,20 +38,24 @@ class DataStoreManager(private val context: Context) { ) suspend fun init() { - try { - context.dataStore.data.first() - } catch (e: IOException) { - Timber.e(e) + withContext(ioDispatcher) { + try { + context.dataStore.data.first() + } catch (e: IOException) { + Timber.e(e) + } } } suspend fun saveToDataStore(key: Preferences.Key, value: T) { - try { - context.dataStore.edit { it[key] = value } - } catch (e: IOException) { - Timber.e(e) - } catch (e: Exception) { - Timber.e(e) + withContext(ioDispatcher) { + try { + context.dataStore.edit { it[key] = value } + } catch (e: IOException) { + Timber.e(e) + } catch (e: Exception) { + Timber.e(e) + } } } @@ -53,11 +63,13 @@ class DataStoreManager(private val context: Context) { fun getFromStoreFlow(key: Preferences.Key) = context.dataStore.data.map { it[key] } suspend fun getFromStore(key: Preferences.Key): T? { - return try { - context.dataStore.data.map { it[key] }.first() - } catch (e: IOException) { - Timber.e(e) - null + return withContext(ioDispatcher) { + try { + context.dataStore.data.map { it[key] }.first() + } catch (e: IOException) { + Timber.e(e) + null + } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt index a35bb15..efc6f5c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt @@ -40,12 +40,14 @@ data class TunnelConfig( Config.parse(it) } } - fun configFromAmQuick(amQuick: String) : org.amnezia.awg.config.Config { + + fun configFromAmQuick(amQuick: String): org.amnezia.awg.config.Config { val inputStream: InputStream = amQuick.byteInputStream() return inputStream.bufferedReader(Charsets.UTF_8).use { org.amnezia.awg.config.Config.parse(it) } } + const val AM_QUICK_DEFAULT = "" } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepository.kt index 6e4503d..0261322 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepository.kt @@ -22,7 +22,7 @@ interface TunnelConfigRepository { suspend fun count(): Int - suspend fun findByTunnelName(name : String) : TunnelConfig? + suspend fun findByTunnelName(name: String): TunnelConfig? suspend fun findByTunnelNetworksName(name: String): TunnelConfigs diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/AppModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/AppModule.kt new file mode 100644 index 0000000..788a052 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/AppModule.kt @@ -0,0 +1,30 @@ +package com.zaneschepke.wireguardautotunnel.module + +import android.content.Context +import com.zaneschepke.logcatter.LocalLogCollector +import com.zaneschepke.logcatter.LogcatHelper +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class AppModule { + @Singleton + @ApplicationScope + @Provides + fun providesApplicationScope(@DefaultDispatcher defaultDispatcher: CoroutineDispatcher): CoroutineScope = + CoroutineScope(SupervisorJob() + defaultDispatcher) + + @Singleton + @Provides + fun provideLogCollect(@ApplicationContext context: Context): LocalLogCollector { + return LogcatHelper.init(context = context) + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/Userspace.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/BackendQualifiers.kt similarity index 68% rename from app/src/main/java/com/zaneschepke/wireguardautotunnel/module/Userspace.kt rename to app/src/main/java/com/zaneschepke/wireguardautotunnel/module/BackendQualifiers.kt index 8a85a7d..88d7443 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/Userspace.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/BackendQualifiers.kt @@ -2,6 +2,10 @@ package com.zaneschepke.wireguardautotunnel.module import javax.inject.Qualifier +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class Kernel + @Qualifier @Retention(AnnotationRetention.BINARY) annotation class Userspace diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/CoroutineQualifiers.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/CoroutineQualifiers.kt new file mode 100644 index 0000000..e94e690 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/CoroutineQualifiers.kt @@ -0,0 +1,27 @@ +package com.zaneschepke.wireguardautotunnel.module + +import javax.inject.Qualifier + +@Retention(AnnotationRetention.RUNTIME) +@Qualifier +annotation class DefaultDispatcher + +@Retention(AnnotationRetention.RUNTIME) +@Qualifier +annotation class IoDispatcher + +@Retention(AnnotationRetention.RUNTIME) +@Qualifier +annotation class MainDispatcher + +@Retention(AnnotationRetention.BINARY) +@Qualifier +annotation class MainImmediateDispatcher + +@Retention(AnnotationRetention.RUNTIME) +@Qualifier +annotation class ApplicationScope + +@Retention(AnnotationRetention.RUNTIME) +@Qualifier +annotation class ServiceScope diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/CoroutinesDispatchersModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/CoroutinesDispatchersModule.kt new file mode 100644 index 0000000..40f1063 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/CoroutinesDispatchersModule.kt @@ -0,0 +1,28 @@ +package com.zaneschepke.wireguardautotunnel.module + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +@Module +@InstallIn(SingletonComponent::class) +object CoroutinesDispatchersModule { + @DefaultDispatcher + @Provides + fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default + + @IoDispatcher + @Provides + fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO + + @MainDispatcher + @Provides + fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main + + @MainImmediateDispatcher + @Provides + fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/DatabaseModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/DatabaseModule.kt deleted file mode 100644 index be87f9e..0000000 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/DatabaseModule.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.zaneschepke.wireguardautotunnel.module - -import android.content.Context -import androidx.room.Room -import com.zaneschepke.wireguardautotunnel.R -import com.zaneschepke.wireguardautotunnel.data.AppDatabase -import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -class DatabaseModule { - @Provides - @Singleton - fun provideDatabase(@ApplicationContext context: Context): AppDatabase { - return Room.databaseBuilder( - context, - AppDatabase::class.java, - context.getString(R.string.db_name), - ) - .fallbackToDestructiveMigration() - .addCallback(DatabaseCallback()) - .build() - } -} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/Kernel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/Kernel.kt deleted file mode 100644 index a763c09..0000000 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/Kernel.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.zaneschepke.wireguardautotunnel.module - -import javax.inject.Qualifier - -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class Kernel diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/RepositoryModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/RepositoryModule.kt index f786045..a3f2e28 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/RepositoryModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/RepositoryModule.kt @@ -1,7 +1,10 @@ package com.zaneschepke.wireguardautotunnel.module import android.content.Context +import androidx.room.Room +import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.data.AppDatabase +import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback import com.zaneschepke.wireguardautotunnel.data.SettingsDao import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager @@ -18,11 +21,25 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineDispatcher import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) class RepositoryModule { + @Provides + @Singleton + fun provideDatabase(@ApplicationContext context: Context): AppDatabase { + return Room.databaseBuilder( + context, + AppDatabase::class.java, + context.getString(R.string.db_name), + ) + .fallbackToDestructiveMigration() + .addCallback(DatabaseCallback()) + .build() + } + @Singleton @Provides fun provideSettingsDoa(appDatabase: AppDatabase): SettingsDao { @@ -49,8 +66,11 @@ class RepositoryModule { @Singleton @Provides - fun providePreferencesDataStore(@ApplicationContext context: Context): DataStoreManager { - return DataStoreManager(context) + fun providePreferencesDataStore( + @ApplicationContext context: Context, + @IoDispatcher ioDispatcher: CoroutineDispatcher + ): DataStoreManager { + return DataStoreManager(context, ioDispatcher) } @Provides 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 2b319ac..86205b6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt @@ -15,6 +15,8 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope import javax.inject.Singleton @Module @@ -42,7 +44,7 @@ class TunnelModule { @Provides @Singleton - fun provideAmneziaBackend(@ApplicationContext context: Context) : org.amnezia.awg.backend.Backend { + fun provideAmneziaBackend(@ApplicationContext context: Context): org.amnezia.awg.backend.Backend { return org.amnezia.awg.backend.GoBackend(context) } @@ -52,14 +54,26 @@ class TunnelModule { amneziaBackend: org.amnezia.awg.backend.Backend, @Userspace userspaceBackend: Backend, @Kernel kernelBackend: Backend, - appDataRepository: AppDataRepository + appDataRepository: AppDataRepository, + @ApplicationScope applicationScope: CoroutineScope, + @IoDispatcher ioDispatcher: CoroutineDispatcher ): VpnService { - return WireGuardTunnel(amneziaBackend,userspaceBackend, kernelBackend, appDataRepository) + return WireGuardTunnel( + amneziaBackend, + userspaceBackend, + kernelBackend, + appDataRepository, + applicationScope, + ioDispatcher, + ) } @Provides @Singleton - fun provideServiceManager(appDataRepository: AppDataRepository): ServiceManager { - return ServiceManager(appDataRepository) + fun provideServiceManager( + appDataRepository: AppDataRepository, + @IoDispatcher ioDispatcher: CoroutineDispatcher + ): ServiceManager { + return ServiceManager(appDataRepository, ioDispatcher) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ViewModelModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ViewModelModule.kt new file mode 100644 index 0000000..4cbb42e --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ViewModelModule.kt @@ -0,0 +1,25 @@ +package com.zaneschepke.wireguardautotunnel.module + +import android.content.Context +import com.zaneschepke.wireguardautotunnel.util.FileUtils +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.scopes.ViewModelScoped +import kotlinx.coroutines.CoroutineDispatcher + +@Module +@InstallIn(ViewModelComponent::class) +class ViewModelModule { + + @ViewModelScoped + @Provides + fun provideFileUtils( + @ApplicationContext context: Context, + @IoDispatcher ioDispatcher: CoroutineDispatcher + ): FileUtils { + return FileUtils(context, ioDispatcher) + } +} 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 f58e4bb..66dc33e 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 @@ -24,6 +24,7 @@ open class ForegroundService : LifecycleService() { when (action) { Action.START.name, Action.START_FOREGROUND.name -> startService(intent.extras) + Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService() Constants.ALWAYS_ON_VPN_ACTION -> { Timber.i("Always-on VPN starting service") 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 ec7129d..f825bf6 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 @@ -4,10 +4,16 @@ import android.app.Service import android.content.Context import android.content.Intent import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository +import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.util.Constants +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext import timber.log.Timber -class ServiceManager(private val appDataRepository: AppDataRepository) { +class ServiceManager( + private val appDataRepository: AppDataRepository, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher +) { private fun actionOnService( action: Action, @@ -23,7 +29,10 @@ class ServiceManager(private val appDataRepository: AppDataRepository) { intent.component?.javaClass try { when (action) { - Action.START_FOREGROUND, Action.STOP_FOREGROUND -> context.startForegroundService(intent) + Action.START_FOREGROUND, Action.STOP_FOREGROUND -> context.startForegroundService( + intent, + ) + Action.START, Action.STOP -> context.startService(intent) } } catch (e: Exception) { @@ -46,23 +55,27 @@ class ServiceManager(private val appDataRepository: AppDataRepository) { } suspend fun stopVpnServiceForeground(context: Context, isManualStop: Boolean = false) { - if (isManualStop) onManualStop() - Timber.i("Stopping vpn service") - actionOnService( - Action.STOP_FOREGROUND, - context, - WireGuardTunnelService::class.java, - ) + withContext(ioDispatcher) { + if (isManualStop) onManualStop() + Timber.i("Stopping vpn service") + actionOnService( + Action.STOP_FOREGROUND, + context, + WireGuardTunnelService::class.java, + ) + } } suspend fun stopVpnService(context: Context, isManualStop: Boolean = false) { - if (isManualStop) onManualStop() - Timber.i("Stopping vpn service") - actionOnService( - Action.STOP, - context, - WireGuardTunnelService::class.java, - ) + withContext(ioDispatcher) { + if (isManualStop) onManualStop() + Timber.i("Stopping vpn service") + actionOnService( + Action.STOP, + context, + WireGuardTunnelService::class.java, + ) + } } private suspend fun onManualStop() { @@ -80,13 +93,15 @@ class ServiceManager(private val appDataRepository: AppDataRepository) { tunnelId: Int? = null, isManualStart: Boolean = false ) { - if (isManualStart) onManualStart(tunnelId) - actionOnService( - Action.START_FOREGROUND, - context, - WireGuardTunnelService::class.java, - tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) }, - ) + withContext(ioDispatcher) { + if (isManualStart) onManualStart(tunnelId) + actionOnService( + Action.START_FOREGROUND, + context, + WireGuardTunnelService::class.java, + tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) }, + ) + } } fun startWatcherServiceForeground( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WatcherState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WatcherState.kt index 5ac2f72..c450529 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WatcherState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WatcherState.kt @@ -21,13 +21,6 @@ data class WatcherState( isMobileDataConnected) } - fun isTunnelOnMobileDataPreferredConditionMet(): Boolean { - return (!isEthernetConnected && - settings.isTunnelOnMobileDataEnabled && - !isWifiConnected && - isMobileDataConnected) - } - fun isTunnelOffOnMobileDataConditionMet(): Boolean { return (!isEthernetConnected && !settings.isTunnelOnMobileDataEnabled && 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 051174f..36ea1f0 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 @@ -8,6 +8,8 @@ import androidx.lifecycle.lifecycleScope import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository +import com.zaneschepke.wireguardautotunnel.module.IoDispatcher +import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher import com.zaneschepke.wireguardautotunnel.service.network.EthernetService import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService import com.zaneschepke.wireguardautotunnel.service.network.NetworkService @@ -19,13 +21,14 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.util.Constants import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber import java.net.InetAddress import javax.inject.Inject @@ -56,6 +59,14 @@ class WireGuardConnectivityWatcherService : ForegroundService() { @Inject lateinit var serviceManager: ServiceManager + @Inject + @IoDispatcher + lateinit var ioDispatcher: CoroutineDispatcher + + @Inject + @MainImmediateDispatcher + lateinit var mainImmediateDispatcher: CoroutineDispatcher + private val networkEventsFlow = MutableStateFlow(WatcherState()) private var watcherJob: Job? = null @@ -65,7 +76,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() { override fun onCreate() { super.onCreate() - lifecycleScope.launch(Dispatchers.Main) { + lifecycleScope.launch(mainImmediateDispatcher) { try { if (appDataRepository.settings.getSettings().isAutoTunnelPaused) { launchWatcherPausedNotification() @@ -138,14 +149,14 @@ class WireGuardConnectivityWatcherService : ForegroundService() { private fun cancelWatcherJob() { try { watcherJob?.cancel() - } catch (e : CancellationException) { + } catch (e: CancellationException) { Timber.i("Watcher job cancelled") } } private fun startWatcherJob() { watcherJob = - lifecycleScope.launch(Dispatchers.IO) { + lifecycleScope.launch { val setting = appDataRepository.settings.getSettings() launch { Timber.i("Starting wifi watcher") @@ -182,69 +193,74 @@ class WireGuardConnectivityWatcherService : ForegroundService() { } private suspend fun watchForMobileDataConnectivityChanges() { - mobileDataService.networkStatus.collect { status -> - when (status) { - is NetworkStatus.Available -> { - Timber.i("Gained Mobile data connection") - networkEventsFlow.update { - it.copy( - isMobileDataConnected = true, - ) + withContext(ioDispatcher) { + mobileDataService.networkStatus.collect { status -> + when (status) { + is NetworkStatus.Available -> { + Timber.i("Gained Mobile data connection") + networkEventsFlow.update { + it.copy( + isMobileDataConnected = true, + ) + } } - } - is NetworkStatus.CapabilitiesChanged -> { - networkEventsFlow.update { - it.copy( - isMobileDataConnected = true, - ) + is NetworkStatus.CapabilitiesChanged -> { + networkEventsFlow.update { + it.copy( + isMobileDataConnected = true, + ) + } + Timber.i("Mobile data capabilities changed") } - Timber.i("Mobile data capabilities changed") - } - is NetworkStatus.Unavailable -> { - networkEventsFlow.update { - it.copy( - isMobileDataConnected = false, - ) + is NetworkStatus.Unavailable -> { + networkEventsFlow.update { + it.copy( + isMobileDataConnected = false, + ) + } + Timber.i("Lost mobile data connection") } - Timber.i("Lost mobile data connection") } } } } private suspend fun watchForPingFailure() { - try { - do { - if (vpnService.vpnState.value.status == TunnelState.UP) { - val tunnelConfig = vpnService.vpnState.value.tunnelConfig - tunnelConfig?.let { - val config = TunnelConfig.configFromWgQuick(it.wgQuick) - val results = config.peers.map { peer -> - val host = if (peer.endpoint.isPresent && - peer.endpoint.get().resolved.isPresent) - peer.endpoint.get().resolved.get().host - else Constants.DEFAULT_PING_IP - Timber.i("Checking reachability of: $host") - val reachable = InetAddress.getByName(host) - .isReachable(Constants.PING_TIMEOUT.toInt()) - Timber.i("Result: reachable - $reachable") - reachable - } - if (results.contains(false)) { - Timber.i("Restarting VPN for ping failure") - serviceManager.stopVpnServiceForeground(this) - delay(Constants.VPN_RESTART_DELAY) - serviceManager.startVpnServiceForeground(this, it.id) - delay(Constants.PING_COOLDOWN) + val context = this + withContext(ioDispatcher) { + try { + do { + if (vpnService.vpnState.value.status == TunnelState.UP) { + val tunnelConfig = vpnService.vpnState.value.tunnelConfig + tunnelConfig?.let { + val config = TunnelConfig.configFromWgQuick(it.wgQuick) + val results = config.peers.map { peer -> + val host = if (peer.endpoint.isPresent && + peer.endpoint.get().resolved.isPresent) + peer.endpoint.get().resolved.get().host + else Constants.DEFAULT_PING_IP + Timber.i("Checking reachability of: $host") + val reachable = InetAddress.getByName(host) + .isReachable(Constants.PING_TIMEOUT.toInt()) + Timber.i("Result: reachable - $reachable") + reachable + } + if (results.contains(false)) { + Timber.i("Restarting VPN for ping failure") + serviceManager.stopVpnServiceForeground(context) + delay(Constants.VPN_RESTART_DELAY) + serviceManager.startVpnServiceForeground(context, it.id) + delay(Constants.PING_COOLDOWN) + } } } - } - delay(Constants.PING_INTERVAL) - } while (true) - } catch (e: Exception) { - Timber.e(e) + delay(Constants.PING_INTERVAL) + } while (true) + } catch (e: Exception) { + Timber.e(e) + } } } @@ -265,77 +281,82 @@ class WireGuardConnectivityWatcherService : ForegroundService() { } private suspend fun watchForEthernetConnectivityChanges() { - ethernetService.networkStatus.collect { status -> - when (status) { - is NetworkStatus.Available -> { - Timber.i("Gained Ethernet connection") - networkEventsFlow.update { - it.copy( - isEthernetConnected = true, - ) + withContext(ioDispatcher) { + ethernetService.networkStatus.collect { status -> + when (status) { + is NetworkStatus.Available -> { + Timber.i("Gained Ethernet connection") + networkEventsFlow.update { + it.copy( + isEthernetConnected = true, + ) + } } - } - is NetworkStatus.CapabilitiesChanged -> { - Timber.i("Ethernet capabilities changed") - networkEventsFlow.update { - it.copy( - isEthernetConnected = true, - ) + is NetworkStatus.CapabilitiesChanged -> { + Timber.i("Ethernet capabilities changed") + networkEventsFlow.update { + it.copy( + isEthernetConnected = true, + ) + } } - } - is NetworkStatus.Unavailable -> { - networkEventsFlow.update { - it.copy( - isEthernetConnected = false, - ) + is NetworkStatus.Unavailable -> { + networkEventsFlow.update { + it.copy( + isEthernetConnected = false, + ) + } + Timber.i("Lost Ethernet connection") } - Timber.i("Lost Ethernet connection") } } } } private suspend fun watchForWifiConnectivityChanges() { - wifiService.networkStatus.collect { status -> - when (status) { - is NetworkStatus.Available -> { - Timber.i("Gained Wi-Fi connection") - networkEventsFlow.update { - it.copy( - isWifiConnected = true, - ) - } - } - is NetworkStatus.CapabilitiesChanged -> { - Timber.i("Wifi capabilities changed") - networkEventsFlow.update { - it.copy( - isWifiConnected = true, - ) - } - val ssid = wifiService.getNetworkName(status.networkCapabilities) - ssid?.let { name -> - if(name.contains(Constants.UNREADABLE_SSID)) { - Timber.w("SSID unreadable: missing permissions") - } else Timber.i("Detected valid SSID") - appDataRepository.appState.setCurrentSsid(name) + withContext(ioDispatcher) { + wifiService.networkStatus.collect { status -> + when (status) { + is NetworkStatus.Available -> { + Timber.i("Gained Wi-Fi connection") networkEventsFlow.update { it.copy( - currentNetworkSSID = name, + isWifiConnected = true, ) } - } ?: Timber.w("Failed to read ssid") - } - - is NetworkStatus.Unavailable -> { - networkEventsFlow.update { - it.copy( - isWifiConnected = false, - ) } - Timber.i("Lost Wi-Fi connection") + + is NetworkStatus.CapabilitiesChanged -> { + Timber.i("Wifi capabilities changed") + networkEventsFlow.update { + it.copy( + isWifiConnected = true, + ) + } + val ssid = wifiService.getNetworkName(status.networkCapabilities) + ssid?.let { name -> + if (name.contains(Constants.UNREADABLE_SSID)) { + Timber.w("SSID unreadable: missing permissions") + } else Timber.i("Detected valid SSID") + appDataRepository.appState.setCurrentSsid(name) + networkEventsFlow.update { + it.copy( + currentNetworkSSID = name, + ) + } + } ?: Timber.w("Failed to read ssid") + } + + is NetworkStatus.Unavailable -> { + networkEventsFlow.update { + it.copy( + isWifiConnected = false, + ) + } + Timber.i("Lost Wi-Fi connection") + } } } } @@ -349,78 +370,89 @@ class WireGuardConnectivityWatcherService : ForegroundService() { return appDataRepository.tunnels.findByTunnelNetworksName(ssid).firstOrNull() } - private fun isTunnelDown() : Boolean { + private fun isTunnelDown(): Boolean { return vpnService.vpnState.value.status == TunnelState.DOWN } private suspend fun manageVpn() { - networkEventsFlow.collectLatest { watcherState -> - val autoTunnel = "Auto-tunnel watcher" - if (!watcherState.settings.isAutoTunnelPaused) { - //delay for rapid network state changes and then collect latest - delay(Constants.WATCHER_COLLECTION_DELAY) - val tunnelConfig = vpnService.vpnState.value.tunnelConfig - when { - watcherState.isEthernetConditionMet() -> { - Timber.i("$autoTunnel - tunnel on on ethernet condition met") - if(isTunnelDown()) serviceManager.startVpnServiceForeground(this) - } + val context = this + withContext(ioDispatcher) { + networkEventsFlow.collectLatest { watcherState -> + val autoTunnel = "Auto-tunnel watcher" + if (!watcherState.settings.isAutoTunnelPaused) { + //delay for rapid network state changes and then collect latest + delay(Constants.WATCHER_COLLECTION_DELAY) + val tunnelConfig = vpnService.vpnState.value.tunnelConfig + when { + watcherState.isEthernetConditionMet() -> { + Timber.i("$autoTunnel - tunnel on on ethernet condition met") + if (isTunnelDown()) serviceManager.startVpnServiceForeground(context) + } - watcherState.isMobileDataConditionMet() -> { - Timber.i("$autoTunnel - tunnel on on mobile data condition met") - if(isTunnelDown()) serviceManager.startVpnServiceForeground(this, getMobileDataTunnel()?.id) - } - - watcherState.isTunnelOnMobileDataPreferredConditionMet() -> { - if(tunnelConfig?.isMobileDataTunnel == false) { - getMobileDataTunnel()?.let { + watcherState.isMobileDataConditionMet() -> { + Timber.i("$autoTunnel - tunnel on on mobile data condition met") + val mobileDataTunnel = getMobileDataTunnel() + val tunnel = + mobileDataTunnel ?: appDataRepository.getPrimaryOrFirstTunnel() + if (isTunnelDown()) return@collectLatest serviceManager.startVpnServiceForeground( + context, + tunnel?.id, + ) + if (tunnelConfig?.isMobileDataTunnel == false && mobileDataTunnel != null) { Timber.i("$autoTunnel - tunnel connected on mobile data is not preferred condition met, switching to preferred") - if(isTunnelDown()) serviceManager.startVpnServiceForeground( - this, - getMobileDataTunnel()?.id, + serviceManager.startVpnServiceForeground( + context, + mobileDataTunnel.id, ) } } - } - watcherState.isTunnelOffOnMobileDataConditionMet() -> { - Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off") - if(!isTunnelDown()) serviceManager.stopVpnServiceForeground(this) - } - - watcherState.isUntrustedWifiConditionMet() -> { - if(tunnelConfig?.tunnelNetworks?.contains(watcherState.currentNetworkSSID) == false || - tunnelConfig == null) { - Timber.i("$autoTunnel - tunnel on ssid not associated with current tunnel condition met") - getSsidTunnel(watcherState.currentNetworkSSID)?.let { - Timber.i("Found tunnel associated with this SSID, bringing tunnel up") - if(isTunnelDown()) serviceManager.startVpnServiceForeground(this, it.id) - } ?: suspend { - Timber.i("No tunnel associated with this SSID, using defaults") - if (appDataRepository.getPrimaryOrFirstTunnel()?.name != vpnService.name) { - if(isTunnelDown()) serviceManager.startVpnServiceForeground(this) - } - }.invoke() + watcherState.isTunnelOffOnMobileDataConditionMet() -> { + Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off") + if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context) } - } - watcherState.isTrustedWifiConditionMet() -> { - Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off") - if(!isTunnelDown()) serviceManager.stopVpnServiceForeground(this) - } + watcherState.isUntrustedWifiConditionMet() -> { + if (tunnelConfig?.tunnelNetworks?.contains(watcherState.currentNetworkSSID) == false || + tunnelConfig == null) { + Timber.i("$autoTunnel - tunnel on ssid not associated with current tunnel condition met") + getSsidTunnel(watcherState.currentNetworkSSID)?.let { + Timber.i("Found tunnel associated with this SSID, bringing tunnel up") + if (isTunnelDown()) serviceManager.startVpnServiceForeground( + context, + it.id, + ) + } ?: suspend { + Timber.i("No tunnel associated with this SSID, using defaults") + val default = appDataRepository.getPrimaryOrFirstTunnel() + if (default?.name != vpnService.name) { + default?.let { + serviceManager.startVpnServiceForeground(context, it.id) + } - watcherState.isTunnelOffOnWifiConditionMet() -> { - Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off") - if(!isTunnelDown()) serviceManager.stopVpnServiceForeground(this) - } + } + }.invoke() + } + } - watcherState.isTunnelOffOnNoConnectivityMet() -> { - Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off") - if(!isTunnelDown()) serviceManager.stopVpnServiceForeground(this) - } + watcherState.isTrustedWifiConditionMet() -> { + Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off") + if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context) + } - else -> { - Timber.i("$autoTunnel - no condition met") + watcherState.isTunnelOffOnWifiConditionMet() -> { + Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off") + if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context) + } + + watcherState.isTunnelOffOnNoConnectivityMet() -> { + Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off") + if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context) + } + + else -> { + Timber.i("$autoTunnel - no condition met") + } } } } 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 6ad524e..5ff3d8c 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 @@ -7,6 +7,8 @@ import androidx.core.app.ServiceCompat import androidx.lifecycle.lifecycleScope import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository +import com.zaneschepke.wireguardautotunnel.module.IoDispatcher +import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus @@ -17,10 +19,11 @@ import com.zaneschepke.wireguardautotunnel.util.handshakeStatus import com.zaneschepke.wireguardautotunnel.util.mapPeerStats import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject @@ -37,13 +40,21 @@ class WireGuardTunnelService : ForegroundService() { @Inject lateinit var notificationService: NotificationService + @Inject + @MainImmediateDispatcher + lateinit var mainImmediateDispatcher: CoroutineDispatcher + + @Inject + @IoDispatcher + lateinit var ioDispatcher: CoroutineDispatcher + private var job: Job? = null private var didShowConnected = false override fun onCreate() { super.onCreate() - lifecycleScope.launch(Dispatchers.Main) { + lifecycleScope.launch(mainImmediateDispatcher) { //TODO fix this to not launch if AOVPN if (appDataRepository.tunnels.count() != 0) { launchVpnNotification() @@ -55,7 +66,7 @@ class WireGuardTunnelService : ForegroundService() { super.startService(extras) cancelJob() job = - lifecycleScope.launch(Dispatchers.IO) { + lifecycleScope.launch { launch { val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY) if (vpnService.getState() == TunnelState.UP) { @@ -75,39 +86,41 @@ class WireGuardTunnelService : ForegroundService() { //TODO improve tunnel notifications private suspend fun handshakeNotifications() { - var tunnelName: String? = null - vpnService.vpnState.collect { state -> + withContext(ioDispatcher) { + var tunnelName: String? = null + vpnService.vpnState.collect { state -> state.statistics - ?.mapPeerStats() - ?.map { it.value?.handshakeStatus() } - .let { statuses -> - when { - statuses?.all { it == HandshakeStatus.HEALTHY } == true -> { - if (!didShowConnected) { - delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY) - tunnelName = state.tunnelConfig?.name - launchVpnNotification( - getString(R.string.tunnel_start_title), - "${getString(R.string.tunnel_start_text)} - $tunnelName", - ) - didShowConnected = true + ?.mapPeerStats() + ?.map { it.value?.handshakeStatus() } + .let { statuses -> + when { + statuses?.all { it == HandshakeStatus.HEALTHY } == true -> { + if (!didShowConnected) { + delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY) + tunnelName = state.tunnelConfig?.name + launchVpnNotification( + getString(R.string.tunnel_start_title), + "${getString(R.string.tunnel_start_text)} - $tunnelName", + ) + didShowConnected = true + } } - } - statuses?.any { it == HandshakeStatus.STALE } == true -> {} - statuses?.all { it == HandshakeStatus.NOT_STARTED } == - true -> { - } + statuses?.any { it == HandshakeStatus.STALE } == true -> {} + statuses?.all { it == HandshakeStatus.NOT_STARTED } == + true -> { + } - else -> {} + else -> {} + } } + if (state.status == TunnelState.UP && state.tunnelConfig?.name != tunnelName) { + tunnelName = state.tunnelConfig?.name + launchVpnNotification( + getString(R.string.tunnel_start_title), + "${getString(R.string.tunnel_start_text)} - $tunnelName", + ) } - if (state.status == TunnelState.UP && state.tunnelConfig?.name != tunnelName) { - tunnelName = state.tunnelConfig?.name - launchVpnNotification( - getString(R.string.tunnel_start_title), - "${getString(R.string.tunnel_start_text)} - $tunnelName", - ) } } } @@ -121,7 +134,7 @@ class WireGuardTunnelService : ForegroundService() { override fun stopService() { super.stopService() - lifecycleScope.launch(Dispatchers.IO) { + lifecycleScope.launch { vpnService.stopTunnel() didShowConnected = false } @@ -181,7 +194,7 @@ class WireGuardTunnelService : ForegroundService() { private fun cancelJob() { try { job?.cancel() - } catch (e : CancellationException) { + } catch (e: CancellationException) { Timber.i("Tunnel job cancelled") } } 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 7203e6d..c4f0f05 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 @@ -2,14 +2,14 @@ package com.zaneschepke.wireguardautotunnel.service.shortcut import android.os.Bundle import androidx.activity.ComponentActivity -import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository +import com.zaneschepke.wireguardautotunnel.module.ApplicationScope import com.zaneschepke.wireguardautotunnel.service.foreground.Action import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject @@ -22,9 +22,13 @@ class ShortcutsActivity : ComponentActivity() { @Inject lateinit var serviceManager: ServiceManager + @Inject + @ApplicationScope + lateinit var applicationScope: CoroutineScope + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - WireGuardAutoTunnel.applicationScope.launch(Dispatchers.IO) { + applicationScope.launch { val settings = appDataRepository.settings.getSettings() if (settings.isShortcutsEnabled) { when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/AutoTunnelControlTile.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/AutoTunnelControlTile.kt index e3fb8b9..939bcaf 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/AutoTunnelControlTile.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/AutoTunnelControlTile.kt @@ -6,12 +6,11 @@ import android.service.quicksettings.TileService import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository +import com.zaneschepke.wireguardautotunnel.module.ApplicationScope import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -25,29 +24,30 @@ class AutoTunnelControlTile : TileService() { @Inject lateinit var serviceManager: ServiceManager - private val scope = CoroutineScope(Dispatchers.IO) + @Inject + @ApplicationScope + lateinit var applicationScope: CoroutineScope private var manualStartConfig: TunnelConfig? = null override fun onStartListening() { super.onStartListening() - scope.launch { - appDataRepository.settings.getSettingsFlow().collectLatest { - when (it.isAutoTunnelEnabled) { - true -> { - if (it.isAutoTunnelPaused) { - setInactive() - setTileDescription(this@AutoTunnelControlTile.getString(R.string.paused)) - } else { - setActive() - setTileDescription(this@AutoTunnelControlTile.getString(R.string.active)) - } + applicationScope.launch { + val settings = appDataRepository.settings.getSettings() + when (settings.isAutoTunnelEnabled) { + true -> { + if (settings.isAutoTunnelPaused) { + setInactive() + setTileDescription(this@AutoTunnelControlTile.getString(R.string.paused)) + } else { + setActive() + setTileDescription(this@AutoTunnelControlTile.getString(R.string.active)) } + } - false -> { - setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled)) - setUnavailable() - } + false -> { + setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled)) + setUnavailable() } } } @@ -58,20 +58,10 @@ class AutoTunnelControlTile : TileService() { onStartListening() } - override fun onDestroy() { - super.onDestroy() - scope.cancel() - } - - override fun onTileRemoved() { - super.onTileRemoved() - scope.cancel() - } - override fun onClick() { super.onClick() unlockAndRun { - scope.launch { + applicationScope.launch { try { appDataRepository.toggleWatcherServicePause() } catch (e: Exception) { 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 15b3c66..7cc3c6f 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 @@ -5,12 +5,13 @@ import android.service.quicksettings.Tile import android.service.quicksettings.TileService import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository +import com.zaneschepke.wireguardautotunnel.module.ApplicationScope import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import timber.log.Timber @@ -28,14 +29,19 @@ class TunnelControlTile : TileService() { @Inject lateinit var serviceManager: ServiceManager - private val scope = CoroutineScope(Dispatchers.IO) + @Inject + @ApplicationScope + lateinit var applicationScope: CoroutineScope private var manualStartConfig: TunnelConfig? = null + private var job: Job? = null; + override fun onStartListening() { super.onStartListening() Timber.d("On start listening called") - scope.launch { + //TODO Fix this + if (job == null || job?.isCancelled == true) job = applicationScope.launch { vpnService.vpnState.collect { it -> when (it.status) { TunnelState.UP -> { @@ -52,22 +58,13 @@ class TunnelControlTile : TileService() { setTileDescription(it.name) } ?: setUnavailable() } + else -> setInactive() } } } } - override fun onDestroy() { - super.onDestroy() - scope.cancel() - } - - override fun onTileRemoved() { - super.onTileRemoved() - scope.cancel() - } - override fun onTileAdded() { super.onTileAdded() onStartListening() @@ -76,7 +73,7 @@ class TunnelControlTile : TileService() { override fun onClick() { super.onClick() unlockAndRun { - scope.launch { + applicationScope.launch { try { if (vpnService.getState() == TunnelState.UP) { serviceManager.stopVpnServiceForeground( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/TunnelState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/TunnelState.kt index f95cf2a..24181ad 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/TunnelState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/TunnelState.kt @@ -7,16 +7,16 @@ enum class TunnelState { DOWN, TOGGLE; - fun toWgState() : Tunnel.State { - return when(this) { + fun toWgState(): Tunnel.State { + return when (this) { UP -> Tunnel.State.UP DOWN -> Tunnel.State.DOWN TOGGLE -> Tunnel.State.TOGGLE } } - fun toAmState() : org.amnezia.awg.backend.Tunnel.State { - return when(this) { + fun toAmState(): org.amnezia.awg.backend.Tunnel.State { + return when (this) { UP -> org.amnezia.awg.backend.Tunnel.State.UP DOWN -> org.amnezia.awg.backend.Tunnel.State.DOWN TOGGLE -> org.amnezia.awg.backend.Tunnel.State.TOGGLE @@ -24,15 +24,16 @@ enum class TunnelState { } companion object { - fun from(state: Tunnel.State) : TunnelState { - return when(state) { + fun from(state: Tunnel.State): TunnelState { + return when (state) { Tunnel.State.DOWN -> DOWN Tunnel.State.TOGGLE -> TOGGLE Tunnel.State.UP -> UP } } - fun from(state: org.amnezia.awg.backend.Tunnel.State) : TunnelState { - return when(state) { + + fun from(state: org.amnezia.awg.backend.Tunnel.State): TunnelState { + return when (state) { org.amnezia.awg.backend.Tunnel.State.DOWN -> DOWN org.amnezia.awg.backend.Tunnel.State.TOGGLE -> TOGGLE org.amnezia.awg.backend.Tunnel.State.UP -> UP diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt index 63795e8..f6704cb 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 @@ -6,6 +6,8 @@ import com.wireguard.android.backend.Tunnel.State import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository +import com.zaneschepke.wireguardautotunnel.module.ApplicationScope +import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.module.Kernel import com.zaneschepke.wireguardautotunnel.module.Userspace import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics @@ -13,14 +15,15 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStati import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics import com.zaneschepke.wireguardautotunnel.util.Constants import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.amnezia.awg.backend.Tunnel import timber.log.Timber import javax.inject.Inject @@ -28,15 +31,16 @@ import javax.inject.Inject class WireGuardTunnel @Inject constructor( - private val userspaceAmneziaBackend : org.amnezia.awg.backend.Backend, + private val userspaceAmneziaBackend: org.amnezia.awg.backend.Backend, @Userspace private val userspaceBackend: Backend, @Kernel private val kernelBackend: Backend, private val appDataRepository: AppDataRepository, + @ApplicationScope private val applicationScope: CoroutineScope, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher ) : VpnService { private val _vpnState = MutableStateFlow(VpnState()) override val vpnState: StateFlow = _vpnState.asStateFlow() - private val scope = CoroutineScope(Dispatchers.IO) private var statsJob: Job? = null @@ -47,20 +51,20 @@ constructor( private var backendIsAmneziaUserspace = false init { - scope.launch { + applicationScope.launch(ioDispatcher) { appDataRepository.settings.getSettingsFlow().collect { if (it.isKernelEnabled && (backendIsWgUserspace || backendIsAmneziaUserspace)) { - Timber.d("Setting kernel backend") + Timber.i("Setting kernel backend") backend = kernelBackend backendIsWgUserspace = false backendIsAmneziaUserspace = false } else if (!it.isKernelEnabled && !it.isAmneziaEnabled && !backendIsWgUserspace) { - Timber.d("Setting WireGuard userspace backend") + Timber.i("Setting WireGuard userspace backend") backend = userspaceBackend backendIsWgUserspace = true backendIsAmneziaUserspace = false } else if (it.isAmneziaEnabled && !backendIsAmneziaUserspace) { - Timber.d("Setting Amnezia userspace backend") + Timber.i("Setting Amnezia userspace backend") backendIsAmneziaUserspace = true backendIsWgUserspace = false } @@ -68,11 +72,11 @@ constructor( } } - private fun setState(tunnelConfig: TunnelConfig?, tunnelState: TunnelState) : TunnelState { - return if(backendIsAmneziaUserspace) { + private fun setState(tunnelConfig: TunnelConfig?, tunnelState: TunnelState): TunnelState { + return if (backendIsAmneziaUserspace) { Timber.i("Using Amnezia backend") val config = tunnelConfig?.let { - if(it.amQuick != "") TunnelConfig.configFromAmQuick(it.amQuick) else { + if (it.amQuick != "") TunnelConfig.configFromAmQuick(it.amQuick) else { Timber.w("Using backwards compatible wg config, amnezia specific config not found.") TunnelConfig.configFromAmQuick(it.wgQuick) } @@ -92,20 +96,22 @@ constructor( } override suspend fun startTunnel(tunnelConfig: TunnelConfig?): TunnelState { - return try { - //TODO we need better error handling here - val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel() - if (config != null) { - emitTunnelConfig(config) - setState(config, TunnelState.UP) - } else throw Exception("No tunnels") - } catch (e: BackendException) { - Timber.e("Failed to start tunnel with error: ${e.message}") - TunnelState.from(State.DOWN) + return withContext(ioDispatcher) { + try { + //TODO we need better error handling here + val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel() + if (config != null) { + emitTunnelConfig(config) + setState(config, TunnelState.UP) + } else throw Exception("No tunnels") + } catch (e: BackendException) { + Timber.e("Failed to start tunnel with error: ${e.message}") + TunnelState.from(State.DOWN) + } } } - private fun emitTunnelState(state : TunnelState) { + private fun emitTunnelState(state: TunnelState) { _vpnState.tryEmit( _vpnState.value.copy( status = state, @@ -134,21 +140,23 @@ constructor( } override suspend fun stopTunnel() { - try { - if (getState() == TunnelState.UP) { - val state = setState(null, TunnelState.DOWN) - resetVpnState() - emitTunnelState(state) + withContext(ioDispatcher) { + try { + if (getState() == TunnelState.UP) { + val state = setState(null, TunnelState.DOWN) + resetVpnState() + emitTunnelState(state) + } + } catch (e: BackendException) { + Timber.e("Failed to stop wireguard tunnel with error: ${e.message}") + } catch (e: org.amnezia.awg.backend.BackendException) { + Timber.e("Failed to stop amnezia tunnel with error: ${e.message}") } - } catch (e: BackendException) { - Timber.e("Failed to stop wireguard tunnel with error: ${e.message}") - } catch (e: org.amnezia.awg.backend.BackendException) { - Timber.e("Failed to stop amnezia tunnel with error: ${e.message}") } } override fun getState(): TunnelState { - return if(backendIsAmneziaUserspace) TunnelState.from(userspaceAmneziaBackend.getState(this)) + return if (backendIsAmneziaUserspace) TunnelState.from(userspaceAmneziaBackend.getState(this)) else TunnelState.from(backend.getState(this)) } @@ -162,31 +170,31 @@ constructor( } private fun handleStateChange(state: TunnelState) { - val tunnel = this emitTunnelState(state) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate() if (state == TunnelState.UP) { - statsJob = - scope.launch { - while (true) { - if(backendIsAmneziaUserspace) { - emitBackendStatistics(AmneziaStatistics(userspaceAmneziaBackend.getStatistics(tunnel))) - } else { - emitBackendStatistics(WireGuardStatistics(backend.getStatistics(tunnel))) - } - delay(Constants.VPN_STATISTIC_CHECK_INTERVAL) - } - } + statsJob = startTunnelStatisticsJob() } if (state == TunnelState.DOWN) { try { statsJob?.cancel() - } catch (e : CancellationException) { + } catch (e: CancellationException) { Timber.i("Stats job cancelled") } } } + private fun startTunnelStatisticsJob() = applicationScope.launch(ioDispatcher) { + while (true) { + if (backendIsAmneziaUserspace) { + emitBackendStatistics(AmneziaStatistics(userspaceAmneziaBackend.getStatistics(this@WireGuardTunnel))) + } else { + emitBackendStatistics(WireGuardStatistics(backend.getStatistics(this@WireGuardTunnel))) + } + delay(Constants.VPN_STATISTIC_CHECK_INTERVAL) + } + } + override fun onStateChange(state: State) { handleStateChange(TunnelState.from(state)) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/AmneziaStatistics.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/AmneziaStatistics.kt index b995de6..a757322 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/AmneziaStatistics.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/AmneziaStatistics.kt @@ -11,7 +11,7 @@ class AmneziaStatistics(private val statistics: Statistics) : TunnelStatistics() PeerStats( rxBytes = stats.rxBytes, txBytes = stats.txBytes, - latestHandshakeEpochMillis = stats.latestHandshakeEpochMillis + latestHandshakeEpochMillis = stats.latestHandshakeEpochMillis, ) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/TunnelStatistics.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/TunnelStatistics.kt index aeeb004..4c70b34 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/TunnelStatistics.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/TunnelStatistics.kt @@ -2,17 +2,17 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics import org.amnezia.awg.crypto.Key -abstract class TunnelStatistics { +abstract class TunnelStatistics { @JvmRecord data class PeerStats(val rxBytes: Long, val txBytes: Long, val latestHandshakeEpochMillis: Long) abstract fun peerStats(peer: Key): PeerStats? - abstract fun isTunnelStale() : Boolean + abstract fun isTunnelStale(): Boolean abstract fun getPeers(): Array - abstract fun rx() : Long + abstract fun rx(): Long - abstract fun tx() : Long + abstract fun tx(): Long } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/WireGuardStatistics.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/WireGuardStatistics.kt index ae2c363..266c6f8 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/WireGuardStatistics.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/WireGuardStatistics.kt @@ -3,7 +3,7 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics import com.wireguard.android.backend.Statistics import org.amnezia.awg.crypto.Key -class WireGuardStatistics(private val statistics: Statistics) : TunnelStatistics() { +class WireGuardStatistics(private val statistics: Statistics) : TunnelStatistics() { override fun peerStats(peer: Key): PeerStats? { val key = com.wireguard.crypto.Key.fromBase64(peer.toBase64()) val peerStats = statistics.peer(key) @@ -11,7 +11,7 @@ class WireGuardStatistics(private val statistics: Statistics) : TunnelStatistics PeerStats( txBytes = peerStats.txBytes, rxBytes = peerStats.rxBytes, - latestHandshakeEpochMillis = peerStats.latestHandshakeEpochMillis + latestHandshakeEpochMillis = peerStats.latestHandshakeEpochMillis, ) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt index b6ff4e6..440be75 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt @@ -4,25 +4,16 @@ import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.net.Uri -import android.widget.Toast -import androidx.compose.runtime.mutableStateListOf import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.wireguard.android.backend.GoBackend -import com.zaneschepke.logcatter.Logcatter -import com.zaneschepke.logcatter.model.LogMessage import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.util.Constants -import com.zaneschepke.wireguardautotunnel.util.FileUtils import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch import timber.log.Timber -import java.time.Instant import javax.inject.Inject @HiltViewModel @@ -50,7 +41,7 @@ constructor() : ViewModel() { private fun requestPermissions() { _appUiState.update { it.copy( - requestPermissions = true + requestPermissions = true, ) } } @@ -58,12 +49,12 @@ constructor() : ViewModel() { fun permissionsRequested() { _appUiState.update { it.copy( - requestPermissions = false + requestPermissions = false, ) } } - fun openWebPage(url: String, context : Context) { + fun openWebPage(url: String, context: Context) { try { val webpage: Uri = Uri.parse(url) val intent = Intent(Intent.ACTION_VIEW, webpage).apply { @@ -79,7 +70,7 @@ constructor() : ViewModel() { fun onVpnPermissionAccepted() { _appUiState.update { it.copy( - vpnPermissionAccepted = true + vpnPermissionAccepted = true, ) } } @@ -122,33 +113,6 @@ constructor() : ViewModel() { } } - val logs = mutableStateListOf() - - fun readLogCatOutput() = - viewModelScope.launch(viewModelScope.coroutineContext + Dispatchers.IO) { - launch { - Logcatter.logs(callback = { - logs.add(it) - if (logs.size > Constants.LOG_BUFFER_SIZE) { - logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt()) - } - }) - } - } - - fun clearLogs() { - logs.clear() - Logcatter.clear() - } - - fun saveLogsToFile(context: Context) { - val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt" - val content = logs.joinToString(separator = "\n") - FileUtils.saveFileToDownloads(context.applicationContext, content, fileName) - Toast.makeText(context, context.getString(R.string.logs_saved), Toast.LENGTH_SHORT) - .show() - } - fun setNotificationPermissionAccepted(accepted: Boolean) { _appUiState.update { it.copy( 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 d7798c6..359ad28 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt @@ -143,7 +143,7 @@ class MainActivity : AppCompatActivity() { if (!appUiState.vpnPermissionAccepted) { return@LaunchedEffect appViewModel.vpnIntent?.let { vpnActivityResultState.launch( - it + it, ) }!! } @@ -155,7 +155,6 @@ class MainActivity : AppCompatActivity() { appViewModel.setNotificationPermissionAccepted( notificationPermissionState?.status?.isGranted ?: true, ) - if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) appViewModel.readLogCatOutput() } LaunchedEffect(appUiState.snackbarMessageConsumed) { @@ -237,30 +236,33 @@ class MainActivity : AppCompatActivity() { ) } composable(Screen.Support.Logs.route) { - LogsScreen(appViewModel) + LogsScreen() } - //TODO fix navigation for amnezia - composable("${Screen.Config.route}/{id}?configType={configType}", arguments = - listOf( - navArgument("id") { - type = NavType.StringType - defaultValue = "0" - }, - navArgument("configType") { - type = NavType.StringType - defaultValue = ConfigType.WIREGUARD.name - } - ) + composable( + "${Screen.Config.route}/{id}?configType={configType}", + arguments = + listOf( + navArgument("id") { + type = NavType.StringType + defaultValue = "0" + }, + navArgument("configType") { + type = NavType.StringType + defaultValue = ConfigType.WIREGUARD.name + }, + ), ) { val id = it.arguments?.getString("id") - val configType = ConfigType.valueOf( it.arguments?.getString("configType") ?: ConfigType.WIREGUARD.name) + val configType = ConfigType.valueOf( + it.arguments?.getString("configType") ?: ConfigType.WIREGUARD.name, + ) if (!id.isNullOrBlank()) { ConfigScreen( navController = navController, tunnelId = id, appViewModel = appViewModel, focusRequester = focusRequester, - configType = configType + configType = configType, ) } } 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 0fa3c57..db2dade 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 @@ -15,6 +15,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics @@ -52,9 +53,10 @@ fun RowListItem( ) { Row( verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(13 / 20f), ) { icon() - Text(text) + Text(text, maxLines = 1, overflow = TextOverflow.Ellipsis) } rowButton() } 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 3e0add3..b9bb4dc 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 @@ -28,12 +28,15 @@ fun ConfigurationToggle( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, ) { - Text(label, textAlign = TextAlign.Start, modifier = Modifier - .weight( - weight = 1.0f, - fill = false, - ), - softWrap = true) + Text( + label, textAlign = TextAlign.Start, + modifier = Modifier + .weight( + weight = 1.0f, + fill = false, + ), + softWrap = true, + ) Switch( modifier = modifier, enabled = enabled, 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 ff9b056..a09833d 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 @@ -483,7 +483,7 @@ fun ConfigScreen( modifier = Modifier.width(IntrinsicSize.Min), ) } - if(configType == ConfigType.AMNEZIA) { + if (configType == ConfigType.AMNEZIA) { ConfigurationTextBox( value = uiState.interfaceProxy.junkPacketCount, onValueChange = { value -> viewModel.onJunkPacketCountChanged(value) }, @@ -496,7 +496,11 @@ fun ConfigScreen( ) ConfigurationTextBox( value = uiState.interfaceProxy.junkPacketMinSize, - onValueChange = { value -> viewModel.onJunkPacketMinSizeChanged(value) }, + onValueChange = { value -> + viewModel.onJunkPacketMinSizeChanged( + value, + ) + }, keyboardActions = keyboardActions, label = stringResource(R.string.junk_packet_minimum_size), hint = stringResource(R.string.junk_packet_minimum_size).lowercase(), @@ -506,7 +510,11 @@ fun ConfigScreen( ) ConfigurationTextBox( value = uiState.interfaceProxy.junkPacketMaxSize, - onValueChange = { value -> viewModel.onJunkPacketMaxSizeChanged(value) }, + onValueChange = { value -> + viewModel.onJunkPacketMaxSizeChanged( + value, + ) + }, keyboardActions = keyboardActions, label = stringResource(R.string.junk_packet_maximum_size), hint = stringResource(R.string.junk_packet_maximum_size).lowercase(), @@ -516,7 +524,11 @@ fun ConfigScreen( ) ConfigurationTextBox( value = uiState.interfaceProxy.initPacketJunkSize, - onValueChange = { value -> viewModel.onInitPacketJunkSizeChanged(value) }, + onValueChange = { value -> + viewModel.onInitPacketJunkSizeChanged( + value, + ) + }, keyboardActions = keyboardActions, label = stringResource(R.string.init_packet_junk_size), hint = stringResource(R.string.init_packet_junk_size).lowercase(), @@ -546,7 +558,11 @@ fun ConfigScreen( ) ConfigurationTextBox( value = uiState.interfaceProxy.responsePacketMagicHeader, - onValueChange = { value -> viewModel.onResponsePacketMagicHeader(value) }, + onValueChange = { value -> + viewModel.onResponsePacketMagicHeader( + value, + ) + }, keyboardActions = keyboardActions, label = stringResource(R.string.response_packet_magic_header), hint = stringResource(R.string.response_packet_magic_header).lowercase(), @@ -556,7 +572,11 @@ fun ConfigScreen( ) ConfigurationTextBox( value = uiState.interfaceProxy.underloadPacketMagicHeader, - onValueChange = { value -> viewModel.onUnderloadPacketMagicHeader(value) }, + onValueChange = { value -> + viewModel.onUnderloadPacketMagicHeader( + value, + ) + }, keyboardActions = keyboardActions, label = stringResource(R.string.underload_packet_magic_header), hint = stringResource(R.string.underload_packet_magic_header).lowercase(), @@ -566,7 +586,11 @@ fun ConfigScreen( ) ConfigurationTextBox( value = uiState.interfaceProxy.transportPacketMagicHeader, - onValueChange = { value -> viewModel.onTransportPacketMagicHeader(value) }, + onValueChange = { value -> + viewModel.onTransportPacketMagicHeader( + value, + ) + }, keyboardActions = keyboardActions, label = stringResource(R.string.transport_packet_magic_header), hint = stringResource(R.string.transport_packet_magic_header).lowercase(), diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigUiState.kt index 718250c..aca6ec4 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigUiState.kt @@ -19,7 +19,7 @@ data class ConfigUiState( val isAmneziaEnabled: Boolean = false ) { companion object { - fun from(config : Config) : ConfigUiState { + fun from(config: Config): ConfigUiState { val proxyPeers = config.peers.map { PeerProxy.from(it) } val proxyInterface = InterfaceProxy.from(config.`interface`) var include = true @@ -43,7 +43,8 @@ data class ConfigUiState( isAllApplicationsEnabled, ) } - fun from(config: org.amnezia.awg.config.Config) : ConfigUiState { + + fun from(config: org.amnezia.awg.config.Config): ConfigUiState { //TODO update with new values val proxyPeers = config.peers.map { PeerProxy.from(it) } val proxyInterface = InterfaceProxy.from(config.`interface`) 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 0bafded..223f79c 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 @@ -16,6 +16,7 @@ import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository +import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType import com.zaneschepke.wireguardautotunnel.util.Constants @@ -25,7 +26,7 @@ import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions import com.zaneschepke.wireguardautotunnel.util.removeAt import com.zaneschepke.wireguardautotunnel.util.update import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -38,7 +39,8 @@ class ConfigViewModel @Inject constructor( private val settingsRepository: SettingsRepository, - private val appDataRepository: AppDataRepository + private val appDataRepository: AppDataRepository, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher ) : ViewModel() { private val packageManager = WireGuardAutoTunnel.instance.packageManager @@ -47,7 +49,7 @@ constructor( val uiState = _uiState.asStateFlow() fun init(tunnelId: String) = - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch(ioDispatcher) { val packages = getQueriedPackages("") val state = if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) { @@ -56,15 +58,16 @@ constructor( .firstOrNull { it.id.toString() == tunnelId } val isAmneziaEnabled = settingsRepository.getSettings().isAmneziaEnabled if (tunnelConfig != null) { - (if(isAmneziaEnabled) { - val amConfig = if(tunnelConfig.amQuick == "") tunnelConfig.wgQuick else tunnelConfig.amQuick + (if (isAmneziaEnabled) { + val amConfig = + if (tunnelConfig.amQuick == "") tunnelConfig.wgQuick else tunnelConfig.amQuick ConfigUiState.from(TunnelConfig.configFromAmQuick(amConfig)) } else ConfigUiState.from(TunnelConfig.configFromWgQuick(tunnelConfig.wgQuick))).copy( packages = packages, loading = false, tunnel = tunnelConfig, tunnelName = tunnelConfig.name, - isAmneziaEnabled = isAmneziaEnabled + isAmneziaEnabled = isAmneziaEnabled, ) } else { ConfigUiState(loading = false, packages = packages) @@ -206,64 +209,82 @@ constructor( if (isAllApplicationsEnabled()) emptyCheckedPackagesList() if (_uiState.value.include) builder.includeApplications(_uiState.value.checkedPackageNames) if (!_uiState.value.include) builder.excludeApplications(_uiState.value.checkedPackageNames) - if(_uiState.value.interfaceProxy.junkPacketCount.isNotEmpty()) { + if (_uiState.value.interfaceProxy.junkPacketCount.isNotEmpty()) { builder.setJunkPacketCount(_uiState.value.interfaceProxy.junkPacketCount.trim().toInt()) } - if(_uiState.value.interfaceProxy.junkPacketMinSize.isNotEmpty()) { - builder.setJunkPacketMinSize(_uiState.value.interfaceProxy.junkPacketMinSize.trim().toInt()) + if (_uiState.value.interfaceProxy.junkPacketMinSize.isNotEmpty()) { + builder.setJunkPacketMinSize( + _uiState.value.interfaceProxy.junkPacketMinSize.trim().toInt(), + ) } - if(_uiState.value.interfaceProxy.junkPacketMaxSize.isNotEmpty()) { - builder.setJunkPacketMaxSize(_uiState.value.interfaceProxy.junkPacketMaxSize.trim().toInt()) + if (_uiState.value.interfaceProxy.junkPacketMaxSize.isNotEmpty()) { + builder.setJunkPacketMaxSize( + _uiState.value.interfaceProxy.junkPacketMaxSize.trim().toInt(), + ) } - if(_uiState.value.interfaceProxy.initPacketJunkSize.isNotEmpty()) { - builder.setInitPacketJunkSize(_uiState.value.interfaceProxy.initPacketJunkSize.trim().toInt()) + if (_uiState.value.interfaceProxy.initPacketJunkSize.isNotEmpty()) { + builder.setInitPacketJunkSize( + _uiState.value.interfaceProxy.initPacketJunkSize.trim().toInt(), + ) } - if(_uiState.value.interfaceProxy.responsePacketJunkSize.isNotEmpty()) { - builder.setResponsePacketJunkSize(_uiState.value.interfaceProxy.responsePacketJunkSize.trim().toInt()) + if (_uiState.value.interfaceProxy.responsePacketJunkSize.isNotEmpty()) { + builder.setResponsePacketJunkSize( + _uiState.value.interfaceProxy.responsePacketJunkSize.trim().toInt(), + ) } - if(_uiState.value.interfaceProxy.initPacketMagicHeader.isNotEmpty()) { - builder.setInitPacketMagicHeader(_uiState.value.interfaceProxy.initPacketMagicHeader.trim().toLong()) + if (_uiState.value.interfaceProxy.initPacketMagicHeader.isNotEmpty()) { + builder.setInitPacketMagicHeader( + _uiState.value.interfaceProxy.initPacketMagicHeader.trim().toLong(), + ) } - if(_uiState.value.interfaceProxy.responsePacketMagicHeader.isNotEmpty()) { - builder.setResponsePacketMagicHeader(_uiState.value.interfaceProxy.responsePacketMagicHeader.trim().toLong()) + if (_uiState.value.interfaceProxy.responsePacketMagicHeader.isNotEmpty()) { + builder.setResponsePacketMagicHeader( + _uiState.value.interfaceProxy.responsePacketMagicHeader.trim().toLong(), + ) } - if(_uiState.value.interfaceProxy.transportPacketMagicHeader.isNotEmpty()) { - builder.setTransportPacketMagicHeader(_uiState.value.interfaceProxy.transportPacketMagicHeader.trim().toLong()) + if (_uiState.value.interfaceProxy.transportPacketMagicHeader.isNotEmpty()) { + builder.setTransportPacketMagicHeader( + _uiState.value.interfaceProxy.transportPacketMagicHeader.trim().toLong(), + ) } - if(_uiState.value.interfaceProxy.underloadPacketMagicHeader.isNotEmpty()) { - builder.setUnderloadPacketMagicHeader(_uiState.value.interfaceProxy.underloadPacketMagicHeader.trim().toLong()) + if (_uiState.value.interfaceProxy.underloadPacketMagicHeader.isNotEmpty()) { + builder.setUnderloadPacketMagicHeader( + _uiState.value.interfaceProxy.underloadPacketMagicHeader.trim().toLong(), + ) } return builder.build() } - private fun buildConfig() : Config { + private fun buildConfig(): Config { val peerList = buildPeerListFromProxyPeers() val wgInterface = buildInterfaceListFromProxyInterface() - return Config.Builder().addPeers(peerList).setInterface(wgInterface).build() + return Config.Builder().addPeers(peerList).setInterface(wgInterface).build() } - private fun buildAmConfig() : org.amnezia.awg.config.Config { + private fun buildAmConfig(): org.amnezia.awg.config.Config { val peerList = buildAmPeerListFromProxyPeers() val amInterface = buildAmInterfaceListFromProxyInterface() - return org.amnezia.awg.config.Config.Builder().addPeers(peerList).setInterface(amInterface).build() + return org.amnezia.awg.config.Config.Builder().addPeers(peerList).setInterface(amInterface) + .build() } fun onSaveAllChanges(configType: ConfigType): Result { return try { val wgQuick = buildConfig().toWgQuickString() - val amQuick = if(configType == ConfigType.AMNEZIA) { + val amQuick = if (configType == ConfigType.AMNEZIA) { buildAmConfig().toAwgQuickString() } else TunnelConfig.AM_QUICK_DEFAULT val tunnelConfig = when (uiState.value.tunnel) { null -> TunnelConfig( name = _uiState.value.tunnelName, wgQuick = wgQuick, - amQuick = amQuick + amQuick = amQuick, ) + else -> uiState.value.tunnel!!.copy( name = _uiState.value.tunnelName, wgQuick = wgQuick, - amQuick = amQuick + amQuick = amQuick, ) } updateTunnelConfig(tunnelConfig) @@ -430,14 +451,15 @@ constructor( fun onJunkPacketCountChanged(value: String) { _uiState.update { it.copy( - interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketCount = value) + interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketCount = value), ) } } + fun onJunkPacketMinSizeChanged(value: String) { _uiState.update { it.copy( - interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketMinSize = value) + interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketMinSize = value), ) } } @@ -445,7 +467,7 @@ constructor( fun onJunkPacketMaxSizeChanged(value: String) { _uiState.update { it.copy( - interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketMaxSize = value) + interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketMaxSize = value), ) } } @@ -453,7 +475,7 @@ constructor( fun onInitPacketJunkSizeChanged(value: String) { _uiState.update { it.copy( - interfaceProxy = _uiState.value.interfaceProxy.copy(initPacketJunkSize = value) + interfaceProxy = _uiState.value.interfaceProxy.copy(initPacketJunkSize = value), ) } } @@ -461,7 +483,7 @@ constructor( fun onResponsePacketJunkSize(value: String) { _uiState.update { it.copy( - interfaceProxy = _uiState.value.interfaceProxy.copy(responsePacketJunkSize = value) + interfaceProxy = _uiState.value.interfaceProxy.copy(responsePacketJunkSize = value), ) } } @@ -469,7 +491,7 @@ constructor( fun onInitPacketMagicHeader(value: String) { _uiState.update { it.copy( - interfaceProxy = _uiState.value.interfaceProxy.copy(initPacketMagicHeader = value) + interfaceProxy = _uiState.value.interfaceProxy.copy(initPacketMagicHeader = value), ) } } @@ -477,7 +499,7 @@ constructor( fun onResponsePacketMagicHeader(value: String) { _uiState.update { it.copy( - interfaceProxy = _uiState.value.interfaceProxy.copy(responsePacketMagicHeader = value) + interfaceProxy = _uiState.value.interfaceProxy.copy(responsePacketMagicHeader = value), ) } } @@ -485,7 +507,7 @@ constructor( fun onTransportPacketMagicHeader(value: String) { _uiState.update { it.copy( - interfaceProxy = _uiState.value.interfaceProxy.copy(transportPacketMagicHeader = value) + interfaceProxy = _uiState.value.interfaceProxy.copy(transportPacketMagicHeader = value), ) } } @@ -493,7 +515,7 @@ constructor( fun onUnderloadPacketMagicHeader(value: String) { _uiState.update { it.copy( - interfaceProxy = _uiState.value.interfaceProxy.copy(underloadPacketMagicHeader = value) + interfaceProxy = _uiState.value.interfaceProxy.copy(underloadPacketMagicHeader = value), ) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/model/InterfaceProxy.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/model/InterfaceProxy.kt index 078511c..86dcbca 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/model/InterfaceProxy.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/model/InterfaceProxy.kt @@ -35,7 +35,8 @@ data class InterfaceProxy( mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "", ) } - fun from(i: org.amnezia.awg.config.Interface) : InterfaceProxy { + + fun from(i: org.amnezia.awg.config.Interface): InterfaceProxy { return InterfaceProxy( publicKey = i.keyPair.publicKey.toBase64().trim(), privateKey = i.keyPair.privateKey.toBase64().trim(), @@ -48,15 +49,24 @@ data class InterfaceProxy( "" }, mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "", - junkPacketCount = if(i.junkPacketCount.isPresent) i.junkPacketCount.get().toString() else "", - junkPacketMinSize = if(i.junkPacketMinSize.isPresent) i.junkPacketMinSize.get().toString() else "", - junkPacketMaxSize = if(i.junkPacketMaxSize.isPresent) i.junkPacketMaxSize.get().toString() else "", - initPacketJunkSize = if(i.initPacketJunkSize.isPresent) i.initPacketJunkSize.get().toString() else "", - responsePacketJunkSize = if(i.responsePacketJunkSize.isPresent) i.responsePacketJunkSize.get().toString() else "", - initPacketMagicHeader = if(i.initPacketMagicHeader.isPresent) i.initPacketMagicHeader.get().toString() else "", - responsePacketMagicHeader = if(i.responsePacketMagicHeader.isPresent) i.responsePacketMagicHeader.get().toString() else "", - transportPacketMagicHeader = if(i.transportPacketMagicHeader.isPresent) i.transportPacketMagicHeader.get().toString() else "", - underloadPacketMagicHeader = if(i.underloadPacketMagicHeader.isPresent) i.underloadPacketMagicHeader.get().toString() else "", + junkPacketCount = if (i.junkPacketCount.isPresent) i.junkPacketCount.get() + .toString() else "", + junkPacketMinSize = if (i.junkPacketMinSize.isPresent) i.junkPacketMinSize.get() + .toString() else "", + junkPacketMaxSize = if (i.junkPacketMaxSize.isPresent) i.junkPacketMaxSize.get() + .toString() else "", + initPacketJunkSize = if (i.initPacketJunkSize.isPresent) i.initPacketJunkSize.get() + .toString() else "", + responsePacketJunkSize = if (i.responsePacketJunkSize.isPresent) i.responsePacketJunkSize.get() + .toString() else "", + initPacketMagicHeader = if (i.initPacketMagicHeader.isPresent) i.initPacketMagicHeader.get() + .toString() else "", + responsePacketMagicHeader = if (i.responsePacketMagicHeader.isPresent) i.responsePacketMagicHeader.get() + .toString() else "", + transportPacketMagicHeader = if (i.transportPacketMagicHeader.isPresent) i.transportPacketMagicHeader.get() + .toString() else "", + underloadPacketMagicHeader = if (i.underloadPacketMagicHeader.isPresent) i.underloadPacketMagicHeader.get() + .toString() else "", ) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/model/PeerProxy.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/model/PeerProxy.kt index 73b0ae1..5223b54 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/model/PeerProxy.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/model/PeerProxy.kt @@ -35,7 +35,7 @@ data class PeerProxy( ) } - fun from(peer: org.amnezia.awg.config.Peer) : PeerProxy { + fun from(peer: org.amnezia.awg.config.Peer): PeerProxy { return PeerProxy( publicKey = peer.publicKey.toBase64(), preSharedKey = 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 e851c24..a70c0de 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 @@ -14,6 +14,7 @@ import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusGroup import androidx.compose.foundation.focusable import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.gestures.detectTapGestures @@ -69,7 +70,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.hapticfeedback.HapticFeedbackType @@ -111,8 +111,6 @@ import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.getMessage import com.zaneschepke.wireguardautotunnel.util.handshakeStatus import com.zaneschepke.wireguardautotunnel.util.mapPeerStats -import com.zaneschepke.wireguardautotunnel.util.truncateWithEllipsis -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -128,7 +126,7 @@ fun MainScreen( val haptic = LocalHapticFeedback.current val context = LocalContext.current val isVisible = rememberSaveable { mutableStateOf(true) } - val scope = rememberCoroutineScope { Dispatchers.IO } + val scope = rememberCoroutineScope() val sheetState = rememberModalBottomSheetState() var showBottomSheet by remember { mutableStateOf(false) } @@ -212,8 +210,8 @@ fun MainScreen( onResult = { if (it.contents != null) { scope.launch { - viewModel.onTunnelQrResult(it.contents, configType).onFailure { - appViewModel.showSnackbarMessage(it.getMessage(context)) + viewModel.onTunnelQrResult(it.contents, configType).onFailure { error -> + appViewModel.showSnackbarMessage(error.getMessage(context)) } } } @@ -246,7 +244,9 @@ fun MainScreen( fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) { if (appViewModel.isRequiredPermissionGranted()) { - if (checked) viewModel.onTunnelStart(tunnel, context) else viewModel.onTunnelStop(context) + if (checked) viewModel.onTunnelStart(tunnel, context) else viewModel.onTunnelStop( + context, + ) } } @@ -270,7 +270,7 @@ fun MainScreen( Scaffold( modifier = Modifier.pointerInput(Unit) { - if(uiState.tunnels.isNotEmpty()) { + if (uiState.tunnels.isNotEmpty()) { detectTapGestures( onTap = { selectedTunnel = null @@ -285,31 +285,25 @@ fun MainScreen( visible = isVisible.value, enter = slideInVertically(initialOffsetY = { it * 2 }), exit = slideOutVertically(targetOffsetY = { it * 2 }), + modifier = Modifier + .focusRequester(focusRequester) + .focusGroup(), ) { val secondaryColor = MaterialTheme.colorScheme.secondary - val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) - var fobColor by remember { mutableStateOf(secondaryColor) } + val tvFobColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) + val fobColor = + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) tvFobColor else secondaryColor + val fobIconColor = + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) Color.White else MaterialTheme.colorScheme.background MultiFloatingActionButton( - modifier = - (if ( - WireGuardAutoTunnel.isRunningOnAndroidTv() && - uiState.tunnels.isEmpty() - ) - Modifier.focusRequester(focusRequester) - else Modifier) - .onFocusChanged { - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - fobColor = if (it.isFocused) hoverColor else secondaryColor - } - }, fabIcon = FabIcon( iconRes = R.drawable.add, iconResAfterRotate = R.drawable.close, - iconRotate = 180f + iconRotate = 180f, ), fabOption = FabOption( - iconTint = MaterialTheme.colorScheme.background, - backgroundTint = MaterialTheme.colorScheme.primary, + iconTint = fobIconColor, + backgroundTint = fobColor, ), itemsMultiFab = listOf( MultiFabItem( @@ -318,24 +312,39 @@ fun MainScreen( stringResource(id = R.string.amnezia), color = Color.White, textAlign = TextAlign.Center, - modifier = Modifier.padding(end = 10.dp) + modifier = Modifier.padding(end = 10.dp), ) }, + modifier = Modifier + .size(40.dp), icon = R.drawable.add, value = ConfigType.AMNEZIA.name, + miniFabOption = FabOption( + backgroundTint = fobColor, + fobIconColor, + ), ), MultiFabItem( label = { - Text(stringResource(id = R.string.wireguard), color = Color.White, textAlign = TextAlign.Center, modifier = Modifier.padding(end = 10.dp)) + Text( + stringResource(id = R.string.wireguard), + color = Color.White, + textAlign = TextAlign.Center, + modifier = Modifier.padding(end = 10.dp), + ) }, icon = R.drawable.add, - value = ConfigType.WIREGUARD.name + value = ConfigType.WIREGUARD.name, + miniFabOption = FabOption( + backgroundTint = fobColor, + fobIconColor, + ), ), ), onFabItemClicked = { showBottomSheet = true configType = ConfigType.valueOf(it.value) - }, + }, shape = RoundedCornerShape(16.dp), ) } @@ -343,7 +352,10 @@ fun MainScreen( ) { if (showBottomSheet) { ModalBottomSheet( - onDismissRequest = { showBottomSheet = false }, + onDismissRequest = { + showBottomSheet = false + + }, sheetState = sheetState, ) { // Sheet content @@ -432,34 +444,48 @@ fun MainScreen( flingBehavior = ScrollableDefaults.flingBehavior(), ) { item { - val gettingStarted = buildAnnotatedString { - append(stringResource(id = R.string.see_the)) - append(" ") - pushStringAnnotation(tag = "gettingStarted", annotation = stringResource(id = R.string.getting_started_url)) - withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) { - append(stringResource(id = R.string.getting_started_guide)) - } - pop() - append(" ") - append(stringResource(R.string.unsure_how)) - append(".") - } AnimatedVisibility( - uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) { + uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn(), + ) { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, - modifier = Modifier.padding(top = 100.dp) + modifier = Modifier + .padding(top = 100.dp) + .fillMaxSize(), ) { - Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic) - ClickableText( - modifier = Modifier.padding(vertical = 10.dp, horizontal = 24.dp), - text = gettingStarted, - style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center), - ) { - gettingStarted.getStringAnnotations(tag = "gettingStarted", it, it).firstOrNull()?.let { annotation -> - appViewModel.openWebPage(annotation.item, context) + val gettingStarted = buildAnnotatedString { + append(stringResource(id = R.string.see_the)) + append(" ") + pushStringAnnotation( + tag = "gettingStarted", + annotation = stringResource(id = R.string.getting_started_url), + ) + withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) { + append(stringResource(id = R.string.getting_started_guide)) } + pop() + append(" ") + append(stringResource(R.string.unsure_how)) + append(".") + } + Text( + text = stringResource(R.string.no_tunnels), + fontStyle = FontStyle.Italic, + ) + ClickableText( + modifier = Modifier + .padding(vertical = 10.dp, horizontal = 24.dp), + text = gettingStarted, + style = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ), + ) { + gettingStarted.getStringAnnotations(tag = "gettingStarted", it, it) + .firstOrNull()?.let { annotation -> + appViewModel.openWebPage(annotation.item, context) + } } } } @@ -562,7 +588,7 @@ fun MainScreen( .size(if (icon == circleIcon) 15.dp else 20.dp), ) }, - text = tunnel.name.truncateWithEllipsis(Constants.ALLOWED_DISPLAY_NAME_LENGTH), + text = tunnel.name, onHold = { if ( (uiState.vpnState.status == TunnelState.UP) && 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 d4d45cc..46be06a 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 @@ -11,6 +11,7 @@ import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository +import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.util.Constants @@ -18,7 +19,7 @@ import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions import com.zaneschepke.wireguardautotunnel.util.toWgQuickString import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn @@ -35,7 +36,8 @@ class MainViewModel constructor( private val appDataRepository: AppDataRepository, private val serviceManager: ServiceManager, - val vpnService: VpnService + val vpnService: VpnService, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher ) : ViewModel() { val uiState = @@ -52,13 +54,12 @@ constructor( MainUiState(), ) - private fun stopWatcherService(context: Context) = - viewModelScope.launch(Dispatchers.IO) { - serviceManager.stopWatcherService(context) - } + private fun stopWatcherService(context: Context) { + serviceManager.stopWatcherService(context) + } fun onDelete(tunnel: TunnelConfig, context: Context) { - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch { val settings = appDataRepository.settings.getSettings() val isPrimary = tunnel.isPrimaryTunnel if (appDataRepository.tunnels.count() == 1 || isPrimary) { @@ -80,7 +81,7 @@ constructor( } fun onTunnelStart(tunnelConfig: TunnelConfig, context: Context) = - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch { Timber.d("On start called!") serviceManager.startVpnService( context, @@ -91,41 +92,42 @@ constructor( fun onTunnelStop(context: Context) = - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch { Timber.i("Stopping active tunnel") serviceManager.stopVpnService(context, isManualStop = true) } private fun validateConfigString(config: String, configType: ConfigType) { - when(configType) { + when (configType) { ConfigType.AMNEZIA -> TunnelConfig.configFromAmQuick(config) ConfigType.WIREGUARD -> TunnelConfig.configFromWgQuick(config) } } - private fun generateQrCodeDefaultName(config : String, configType: ConfigType) : String { + private fun generateQrCodeDefaultName(config: String, configType: ConfigType): String { return try { - when(configType) { + when (configType) { ConfigType.AMNEZIA -> { TunnelConfig.configFromAmQuick(config).peers[0].endpoint.get().host } + ConfigType.WIREGUARD -> { TunnelConfig.configFromWgQuick(config).peers[0].endpoint.get().host } } - } catch (e : Exception) { + } catch (e: Exception) { Timber.e(e) NumberUtils.generateRandomTunnelName() } } - private fun generateQrCodeTunnelName(config : String, configType: ConfigType) : String { + private fun generateQrCodeTunnelName(config: String, configType: ConfigType): String { var defaultName = generateQrCodeDefaultName(config, configType) val lines = config.lines().toMutableList() val linesIterator = lines.iterator() - while(linesIterator.hasNext()) { + while (linesIterator.hasNext()) { val next = linesIterator.next() - if(next.contains(Constants.QR_CODE_NAME_PROPERTY)) { + if (next.contains(Constants.QR_CODE_NAME_PROPERTY)) { defaultName = next.substringAfter(Constants.QR_CODE_NAME_PROPERTY).trim() break } @@ -134,121 +136,177 @@ constructor( } suspend fun onTunnelQrResult(result: String, configType: ConfigType): Result { - return try { - validateConfigString(result, configType) - val tunnelName = makeTunnelNameUnique(generateQrCodeTunnelName(result, configType)) - val tunnelConfig = when(configType) { - ConfigType.AMNEZIA ->{ - TunnelConfig(name = tunnelName, amQuick = result, - wgQuick = TunnelConfig.configFromAmQuick(result).toWgQuickString()) + return withContext(ioDispatcher) { + try { + validateConfigString(result, configType) + val tunnelName = makeTunnelNameUnique(generateQrCodeTunnelName(result, configType)) + val tunnelConfig = when (configType) { + ConfigType.AMNEZIA -> { + TunnelConfig( + name = tunnelName, amQuick = result, + wgQuick = TunnelConfig.configFromAmQuick(result).toWgQuickString(), + ) + } + + ConfigType.WIREGUARD -> TunnelConfig(name = tunnelName, wgQuick = result) } - ConfigType.WIREGUARD -> TunnelConfig(name = tunnelName, wgQuick = result) + addTunnel(tunnelConfig) + Result.success(Unit) + } catch (e: Exception) { + Timber.e(e) + Result.failure(WgTunnelExceptions.InvalidQrCode()) } - addTunnel(tunnelConfig) - Result.success(Unit) - } catch (e: Exception) { - Timber.e(e) - Result.failure(WgTunnelExceptions.InvalidQrCode()) } } - private suspend fun makeTunnelNameUnique(name : String) : String { - val tunnels = appDataRepository.tunnels.getAll() - var tunnelName = name - var num = 1 - while (tunnels.any { it.name == tunnelName }) { - tunnelName = name + "(${num})" - num++ + private suspend fun makeTunnelNameUnique(name: String): String { + return withContext(ioDispatcher) { + val tunnels = appDataRepository.tunnels.getAll() + var tunnelName = name + var num = 1 + while (tunnels.any { it.name == tunnelName }) { + tunnelName = name + "(${num})" + num++ + } + tunnelName } - return tunnelName } - private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String, type: ConfigType) { - var amQuick : String? = null + private fun saveTunnelConfigFromStream( + stream: InputStream, + fileName: String, + type: ConfigType + ) { + var amQuick: String? = null val wgQuick = stream.use { - when(type) { + when (type) { ConfigType.AMNEZIA -> { val config = org.amnezia.awg.config.Config.parse(it) amQuick = config.toAwgQuickString() config.toWgQuickString() } + ConfigType.WIREGUARD -> { Config.parse(it).toWgQuickString() } } } - val tunnelName = makeTunnelNameUnique(getNameFromFileName(fileName)) - addTunnel(TunnelConfig(name = tunnelName, wgQuick = wgQuick, amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT)) + viewModelScope.launch { + val tunnelName = makeTunnelNameUnique(getNameFromFileName(fileName)) + addTunnel( + TunnelConfig( + name = tunnelName, + wgQuick = wgQuick, + amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT, + ), + ) + } } private fun getInputStreamFromUri(uri: Uri, context: Context): InputStream? { return context.applicationContext.contentResolver.openInputStream(uri) } - suspend fun onTunnelFileSelected(uri: Uri, configType: ConfigType, context: Context): Result { - return try { - if (isValidUriContentScheme(uri)) { - val fileName = getFileName(context, uri) - return when (getFileExtensionFromFileName(fileName)) { - Constants.CONF_FILE_EXTENSION -> - saveTunnelFromConfUri(fileName, uri, configType, context) - Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri, configType, context) - else -> Result.failure(WgTunnelExceptions.InvalidFileExtension()) + suspend fun onTunnelFileSelected( + uri: Uri, + configType: ConfigType, + context: Context + ): Result { + return withContext(ioDispatcher) { + try { + if (isValidUriContentScheme(uri)) { + val fileName = getFileName(context, uri) + return@withContext when (getFileExtensionFromFileName(fileName)) { + Constants.CONF_FILE_EXTENSION -> + saveTunnelFromConfUri(fileName, uri, configType, context) + + Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri( + uri, + configType, + context, + ) + + else -> Result.failure(WgTunnelExceptions.InvalidFileExtension()) + } + } else { + Result.failure(WgTunnelExceptions.InvalidFileExtension()) } - } else { - Result.failure(WgTunnelExceptions.InvalidFileExtension()) + } catch (e: Exception) { + Timber.e(e) + Result.failure(WgTunnelExceptions.FileReadFailed()) } - } catch (e: Exception) { - Timber.e(e) - Result.failure(WgTunnelExceptions.FileReadFailed()) } } - private suspend fun saveTunnelsFromZipUri(uri: Uri, configType: ConfigType, context: Context) : Result { - return ZipInputStream(getInputStreamFromUri(uri, context)).use { zip -> - generateSequence { zip.nextEntry } - .filterNot { - it.isDirectory || - getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION - } - .forEach { - val name = getNameFromFileName(it.name) - withContext(viewModelScope.coroutineContext + Dispatchers.IO) { - try { - var amQuick : String? = null - val wgQuick = - when(configType) { - ConfigType.AMNEZIA -> { - val config = org.amnezia.awg.config.Config.parse(zip) - amQuick = config.toAwgQuickString() - config.toWgQuickString() + private suspend fun saveTunnelsFromZipUri( + uri: Uri, + configType: ConfigType, + context: Context + ): Result { + return withContext(ioDispatcher) { + ZipInputStream(getInputStreamFromUri(uri, context)).use { zip -> + generateSequence { zip.nextEntry } + .filterNot { + it.isDirectory || + getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION + } + .forEach { + val name = getNameFromFileName(it.name) + withContext(viewModelScope.coroutineContext) { + try { + var amQuick: String? = null + val wgQuick = + when (configType) { + ConfigType.AMNEZIA -> { + val config = org.amnezia.awg.config.Config.parse(zip) + amQuick = config.toAwgQuickString() + config.toWgQuickString() + } + + ConfigType.WIREGUARD -> { + Config.parse(zip).toWgQuickString() + } } - ConfigType.WIREGUARD -> { - Config.parse(zip).toWgQuickString() - } - } - addTunnel(TunnelConfig(name = makeTunnelNameUnique(name), wgQuick = wgQuick, amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT)) - Result.success(Unit) - } catch (e : Exception) { - Result.failure(WgTunnelExceptions.FileReadFailed()) + addTunnel( + TunnelConfig( + name = makeTunnelNameUnique(name), + wgQuick = wgQuick, + amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT, + ), + ) + Result.success(Unit) + } catch (e: Exception) { + Result.failure(WgTunnelExceptions.FileReadFailed()) + } } } + Result.success(Unit) + } + } + } + + private suspend fun saveTunnelFromConfUri( + name: String, + uri: Uri, + configType: ConfigType, + context: Context + ): Result { + return withContext(ioDispatcher) { + val stream = getInputStreamFromUri(uri, context) + return@withContext if (stream != null) { + try { + saveTunnelConfigFromStream(stream, name, configType) + } catch (e: Exception) { + return@withContext Result.failure(WgTunnelExceptions.ConfigParseError()) } - Result.success(Unit) + Result.success(Unit) + } else { + Result.failure(WgTunnelExceptions.FileReadFailed()) + } } } - private suspend fun saveTunnelFromConfUri(name: String, uri: Uri, configType: ConfigType, context: Context): Result { - val stream = getInputStreamFromUri(uri, context) - return if (stream != null) { - saveTunnelConfigFromStream(stream, name, configType) - Result.success(Unit) - } else { - Result.failure(WgTunnelExceptions.FileReadFailed()) - } - } - - private suspend fun addTunnel(tunnelConfig: TunnelConfig) { + private fun addTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch { val firstTunnel = appDataRepository.tunnels.count() == 0 saveTunnel(tunnelConfig) if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate() @@ -266,7 +324,7 @@ constructor( WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate() } - private suspend fun saveTunnel(tunnelConfig: TunnelConfig) { + private fun saveTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch { appDataRepository.tunnels.save(tunnelConfig) } @@ -317,7 +375,7 @@ constructor( } private fun saveSettings(settings: Settings) = - viewModelScope.launch(Dispatchers.IO) { appDataRepository.settings.save(settings) } + viewModelScope.launch { appDataRepository.settings.save(settings) } fun onCopyTunnel(tunnel: TunnelConfig?) = viewModelScope.launch { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt index c2b59d4..e7a66e9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt @@ -1,7 +1,11 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.options import android.annotation.SuppressLint +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusGroup import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -12,6 +16,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions @@ -39,7 +44,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager @@ -114,59 +118,75 @@ fun OptionsScreen( Scaffold( floatingActionButton = { val secondaryColor = MaterialTheme.colorScheme.secondary - val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) - var fobColor by remember { mutableStateOf(secondaryColor) } - MultiFloatingActionButton( - modifier = - (if ( - WireGuardAutoTunnel.isRunningOnAndroidTv() - ) - Modifier.focusRequester(focusRequester) - else Modifier) - .onFocusChanged { - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - fobColor = if (it.isFocused) hoverColor else secondaryColor - } + val tvFobColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) + val fobColor = + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) tvFobColor else secondaryColor + val fobIconColor = + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) Color.White else MaterialTheme.colorScheme.background + AnimatedVisibility( + visible = true, + enter = slideInVertically(initialOffsetY = { it * 2 }), + exit = slideOutVertically(targetOffsetY = { it * 2 }), + modifier = Modifier + .focusRequester(focusRequester) + .focusGroup(), + ) { + MultiFloatingActionButton( + fabIcon = FabIcon( + iconRes = R.drawable.edit, + iconResAfterRotate = R.drawable.close, + iconRotate = 180f, + ), + fabOption = FabOption( + iconTint = fobIconColor, + backgroundTint = fobColor, + ), + itemsMultiFab = listOf( + MultiFabItem( + label = { + Text( + stringResource(id = R.string.amnezia), + color = Color.White, + textAlign = TextAlign.Center, + modifier = Modifier.padding(end = 10.dp), + ) + }, + modifier = Modifier + .size(40.dp), + icon = R.drawable.edit, + value = ConfigType.AMNEZIA.name, + miniFabOption = FabOption( + backgroundTint = fobColor, + fobIconColor, + ), + ), + MultiFabItem( + label = { + Text( + stringResource(id = R.string.wireguard), + color = Color.White, + textAlign = TextAlign.Center, + modifier = Modifier.padding(end = 10.dp), + ) + }, + icon = R.drawable.edit, + value = ConfigType.WIREGUARD.name, + miniFabOption = FabOption( + backgroundTint = fobColor, + fobIconColor, + ), + ), + ), + onFabItemClicked = { + val configType = ConfigType.valueOf(it.value) + navController.navigate( + "${Screen.Config.route}/${tunnelId}?configType=${configType.name}", + ) }, - fabIcon = FabIcon( - iconRes = R.drawable.edit, - iconResAfterRotate = R.drawable.close, - iconRotate = 180f - ), - fabOption = FabOption( - iconTint = MaterialTheme.colorScheme.background, - backgroundTint = MaterialTheme.colorScheme.primary, - ), - itemsMultiFab = listOf( - MultiFabItem( - label = { - Text( - stringResource(id = R.string.amnezia), - color = Color.White, - textAlign = TextAlign.Center, - modifier = Modifier.padding(end = 10.dp) - ) - }, - icon = R.drawable.edit, - value = ConfigType.AMNEZIA.name, - ), - MultiFabItem( - label = { - Text(stringResource(id = R.string.wireguard), color = Color.White, textAlign = TextAlign.Center, modifier = Modifier.padding(end = 10.dp)) - }, - icon = R.drawable.edit, - value = ConfigType.WIREGUARD.name - ), - ), - onFabItemClicked = { - val configType = ConfigType.valueOf(it.value) - navController.navigate( - "${Screen.Config.route}/${tunnelId}?configType=${configType.name}", - ) - }, - shape = RoundedCornerShape(16.dp), - ) - } + shape = RoundedCornerShape(16.dp), + ) + } + }, ) { Column( horizontalAlignment = Alignment.CenterHorizontally, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt index d8fa851..6c5abde 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt @@ -9,7 +9,6 @@ import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine @@ -45,12 +44,12 @@ constructor( fun init(tunnelId: String) { _optionState.update { it.copy( - id = tunnelId + id = tunnelId, ) } } - fun onDeleteRunSSID(ssid: String) = viewModelScope.launch(Dispatchers.IO) { + fun onDeleteRunSSID(ssid: String) = viewModelScope.launch { uiState.value.tunnel?.let { appDataRepository.tunnels.save( tunnelConfig = it.copy( @@ -60,7 +59,7 @@ constructor( } } - private fun saveTunnel(tunnelConfig: TunnelConfig?) = viewModelScope.launch(Dispatchers.IO) { + private fun saveTunnel(tunnelConfig: TunnelConfig?) = viewModelScope.launch { tunnelConfig?.let { appDataRepository.tunnels.save(it) } @@ -81,7 +80,7 @@ constructor( } } - fun onToggleIsMobileDataTunnel() = viewModelScope.launch(Dispatchers.IO) { + fun onToggleIsMobileDataTunnel() = viewModelScope.launch { uiState.value.tunnel?.let { if (it.isMobileDataTunnel) { appDataRepository.tunnels.updateMobileDataTunnel(null) @@ -89,7 +88,7 @@ constructor( } } - fun onTogglePrimaryTunnel() = viewModelScope.launch(Dispatchers.IO) { + fun onTogglePrimaryTunnel() = viewModelScope.launch { if (uiState.value.tunnel != null) { appDataRepository.tunnels.updatePrimaryTunnel( when (uiState.value.isDefaultTunnel) { 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 65812d1..e9e60f3 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 @@ -81,9 +81,7 @@ 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.FileUtils import com.zaneschepke.wireguardautotunnel.util.getMessage -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import timber.log.Timber import xyz.teamgravity.pin_lock_compose.PinManager @@ -100,9 +98,9 @@ fun SettingsScreen( navController: NavController, focusRequester: FocusRequester, ) { - val scope = rememberCoroutineScope { Dispatchers.IO } val context = LocalContext.current val focusManager = LocalFocusManager.current + val scope = rememberCoroutineScope() val scrollState = rememberScrollState() val interactionSource = remember { MutableInteractionSource() } val pinExists = remember { mutableStateOf(PinManager.pinExists()) } @@ -137,18 +135,22 @@ fun SettingsScreen( } file } - val amFiles = uiState.tunnels.mapNotNull { config -> if(config.amQuick != TunnelConfig.AM_QUICK_DEFAULT) { - val file = File(context.cacheDir, "${config.name}-am.conf") - file.outputStream().use { - it.write(config.amQuick.toByteArray()) + val amFiles = uiState.tunnels.mapNotNull { config -> + if (config.amQuick != TunnelConfig.AM_QUICK_DEFAULT) { + val file = File(context.cacheDir, "${config.name}-am.conf") + file.outputStream().use { + it.write(config.amQuick.toByteArray()) + } + file + } else null + } + scope.launch { + viewModel.onExportTunnels(wgFiles + amFiles).onFailure { + appViewModel.showSnackbarMessage(it.getMessage(context)) + }.onSuccess { + didExportFiles = true + appViewModel.showSnackbarMessage(context.getString(R.string.exported_configs_message)) } - file - } else null } - FileUtils.saveFilesToZip(context, wgFiles + amFiles).onFailure { - appViewModel.showSnackbarMessage(it.getMessage(context)) - }.onSuccess { - didExportFiles = true - appViewModel.showSnackbarMessage(context.getString(R.string.exported_configs_message)) } } catch (e: Exception) { Timber.e(e) @@ -190,11 +192,9 @@ fun SettingsScreen( } fun openSettings() { - scope.launch { - val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS) - intentSettings.data = Uri.fromParts("package", context.packageName, null) - context.startActivity(intentSettings) - } + val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS) + intentSettings.data = Uri.fromParts("package", context.packageName, null) + context.startActivity(intentSettings) } fun checkFineLocationGranted() { 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 5c6e89e..7b1d56a 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 @@ -12,6 +12,7 @@ import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.util.Constants +import com.zaneschepke.wireguardautotunnel.util.FileUtils import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted @@ -19,6 +20,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import timber.log.Timber +import java.io.File import javax.inject.Inject @HiltViewModel @@ -28,6 +30,7 @@ constructor( private val appDataRepository: AppDataRepository, private val serviceManager: ServiceManager, private val rootShell: RootShell, + private val fileUtils: FileUtils, vpnService: VpnService ) : ViewModel() { @@ -90,6 +93,10 @@ constructor( ) } + suspend fun onExportTunnels(files: List): Result { + return fileUtils.saveFilesToZip(files) + } + fun onToggleAutoTunnel(context: Context) = viewModelScope.launch { val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled @@ -160,7 +167,7 @@ constructor( } fun onToggleAmnezia() = viewModelScope.launch { - if(uiState.value.settings.isKernelEnabled) { + if (uiState.value.settings.isKernelEnabled) { saveKernelMode(false) } saveAmneziaMode(!uiState.value.settings.isAmneziaEnabled) @@ -169,8 +176,8 @@ constructor( private fun saveAmneziaMode(on: Boolean) { saveSettings( uiState.value.settings.copy( - isAmneziaEnabled = on - ) + isAmneziaEnabled = on, + ), ) } 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 258aacc..284fa34 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 @@ -107,7 +107,12 @@ fun SupportScreen( modifier = Modifier.padding(bottom = 20.dp), ) TextButton( - onClick = { appViewModel.openWebPage(context.resources.getString(R.string.docs_url), context) }, + onClick = { + appViewModel.openWebPage( + context.resources.getString(R.string.docs_url), + context, + ) + }, modifier = Modifier .padding(vertical = 5.dp) .focusRequester(focusRequester), @@ -129,7 +134,7 @@ fun SupportScreen( weight = 1.0f, fill = false, ), - softWrap = true + softWrap = true, ) } Icon( @@ -143,7 +148,12 @@ fun SupportScreen( color = MaterialTheme.colorScheme.onBackground, ) TextButton( - onClick = { appViewModel.openWebPage(context.resources.getString(R.string.telegram_url), context) }, + onClick = { + appViewModel.openWebPage( + context.resources.getString(R.string.telegram_url), + context, + ) + }, modifier = Modifier.padding(vertical = 5.dp), ) { Row( @@ -175,7 +185,12 @@ fun SupportScreen( color = MaterialTheme.colorScheme.onBackground, ) TextButton( - onClick = { appViewModel.openWebPage(context.resources.getString(R.string.github_url), context) }, + onClick = { + appViewModel.openWebPage( + context.resources.getString(R.string.github_url), + context, + ) + }, modifier = Modifier.padding(vertical = 5.dp), ) { Row( @@ -269,7 +284,10 @@ fun SupportScreen( fontSize = 16.sp, modifier = Modifier.clickable { - appViewModel.openWebPage(context.resources.getString(R.string.privacy_policy_url), context) + appViewModel.openWebPage( + context.resources.getString(R.string.privacy_policy_url), + context, + ) }, ) Row( @@ -285,7 +303,7 @@ fun SupportScreen( val mode = buildAnnotatedString { append(stringResource(R.string.mode)) append(": ") - when(uiState.settings.isKernelEnabled){ + when (uiState.settings.isKernelEnabled) { true -> append(stringResource(id = R.string.kernel)) false -> append(stringResource(id = R.string.userspace)) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsScreen.kt index 2de1661..7ad015b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsScreen.kt @@ -1,6 +1,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.support.logs import android.annotation.SuppressLint +import android.widget.Toast import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement @@ -8,7 +9,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -32,17 +33,17 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.zaneschepke.wireguardautotunnel.ui.AppViewModel +import androidx.hilt.navigation.compose.hiltViewModel +import com.zaneschepke.logcatter.model.LogMessage +import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel import kotlinx.coroutines.launch @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable -fun LogsScreen(appViewModel: AppViewModel) { +fun LogsScreen(viewModel: LogsViewModel = hiltViewModel()) { - val logs = remember { - appViewModel.logs - } + val logs = viewModel.logs val context = LocalContext.current @@ -60,7 +61,15 @@ fun LogsScreen(appViewModel: AppViewModel) { floatingActionButton = { FloatingActionButton( onClick = { - appViewModel.saveLogsToFile(context) + scope.launch { + viewModel.saveLogsToFile().onSuccess { + Toast.makeText( + context, + context.getString(R.string.logs_saved), + Toast.LENGTH_SHORT, + ).show() + } + } }, shape = RoundedCornerShape(16.dp), containerColor = MaterialTheme.colorScheme.primary, @@ -82,7 +91,11 @@ fun LogsScreen(appViewModel: AppViewModel) { .fillMaxSize() .padding(horizontal = 24.dp), ) { - items(logs) { + itemsIndexed( + logs, + key = { index, _ -> index }, + contentType = { _: Int, _: LogMessage -> null }, + ) { _, it -> Row( horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start), verticalAlignment = Alignment.Top, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsViewModel.kt new file mode 100644 index 0000000..ae86bb8 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsViewModel.kt @@ -0,0 +1,55 @@ +package com.zaneschepke.wireguardautotunnel.ui.screens.support.logs + +import androidx.compose.runtime.mutableStateListOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.zaneschepke.logcatter.LocalLogCollector +import com.zaneschepke.logcatter.model.LogMessage +import com.zaneschepke.wireguardautotunnel.module.IoDispatcher +import com.zaneschepke.wireguardautotunnel.module.MainDispatcher +import com.zaneschepke.wireguardautotunnel.util.Constants +import com.zaneschepke.wireguardautotunnel.util.FileUtils +import com.zaneschepke.wireguardautotunnel.util.chunked +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.time.Duration +import java.time.Instant +import javax.inject.Inject + +@HiltViewModel +class LogsViewModel +@Inject constructor( + private val localLogCollector: LocalLogCollector, + private val fileUtils: FileUtils, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + @MainDispatcher private val mainDispatcher: CoroutineDispatcher +) : ViewModel() { + + val logs = mutableStateListOf() + + init { + viewModelScope.launch(ioDispatcher) { + localLogCollector.bufferedLogs.chunked(500, Duration.ofSeconds(1)).collect { + withContext(mainDispatcher) { + logs.addAll(it) + } + if (logs.size > Constants.LOG_BUFFER_SIZE) { + withContext(mainDispatcher) { + logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt()) + } + } + } + } + } + + suspend fun saveLogsToFile(): Result { + val file = localLogCollector.getLogFile().getOrElse { + return Result.failure(it) + } + val fileContent = fileUtils.readBytesFromFile(file) + val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt" + return fileUtils.saveByteArrayToDownloads(fileContent, fileName) + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt index 42f162a..9ff90dc 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt @@ -16,10 +16,11 @@ object Constants { const val URI_CONTENT_SCHEME = "content" const val ALLOWED_FILE_TYPES = "*/*" const val TEXT_MIME_TYPE = "text/plain" + const val ZIP_FILE_MIME_TYPE = "application/zip" const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs" const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs" const val ALWAYS_ON_VPN_ACTION = "android.net.VpnService" - const val EMAIL_MIME_TYPE = "message/rfc822" + const val EMAIL_MIME_TYPE = "plain/text" const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024 const val SUBSCRIPTION_TIMEOUT = 5_000L diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt index 3fc3b0e..6c165c0 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt @@ -9,13 +9,27 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.ObsoleteCoroutinesApi +import kotlinx.coroutines.channels.ClosedReceiveChannelException +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.produce +import kotlinx.coroutines.channels.ticker +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.selects.whileSelect import org.amnezia.awg.config.Config +import timber.log.Timber import java.math.BigDecimal import java.text.DecimalFormat +import java.time.Duration +import java.util.concurrent.ConcurrentLinkedQueue import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.cancellation.CancellationException fun BroadcastReceiver.goAsync( context: CoroutineContext = EmptyCoroutineContext, @@ -32,12 +46,6 @@ fun BroadcastReceiver.goAsync( } } -fun String.truncateWithEllipsis(allowedLength: Int): String { - return if (this.length > allowedLength + 3) { - this.substring(0, allowedLength) + "***" - } else this -} - fun BigDecimal.toThreeDecimalPlaceString(): String { val df = DecimalFormat("#.###") return df.format(this) @@ -73,14 +81,14 @@ fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus { } } -fun Config.toWgQuickString() : String { +fun Config.toWgQuickString(): String { val amQuick = toAwgQuickString() val lines = amQuick.lines().toMutableList() val linesIterator = lines.iterator() - while(linesIterator.hasNext()) { + while (linesIterator.hasNext()) { val next = linesIterator.next() Constants.amneziaProperties.forEach { - if(next.startsWith(it, ignoreCase = true)) { + if (next.startsWith(it, ignoreCase = true)) { linesIterator.remove() } } @@ -88,9 +96,73 @@ fun Config.toWgQuickString() : String { return lines.joinToString(System.lineSeparator()) } -fun Throwable.getMessage(context: Context) : String { - return when(this) { +fun Throwable.getMessage(context: Context): String { + return when (this) { is WgTunnelExceptions -> this.getMessage(context) else -> this.message ?: StringValue.StringResource(R.string.unknown_error).asString(context) } } + +/** + * Chunks based on a time or size threshold. + * + * Borrowed from this [Stack Overflow question](https://stackoverflow.com/questions/51022533/kotlin-chunk-sequence-based-on-size-and-time). + */ +@OptIn(ObsoleteCoroutinesApi::class, ExperimentalCoroutinesApi::class) +fun ReceiveChannel.chunked(scope: CoroutineScope, size: Int, time: Duration) = + scope.produce> { + while (true) { // this loop goes over each chunk + val chunk = ConcurrentLinkedQueue() // current chunk + val ticker = ticker(time.toMillis()) // time-limit for this chunk + try { + whileSelect { + ticker.onReceive { + false // done with chunk when timer ticks, takes priority over received elements + } + this@chunked.onReceive { + chunk += it + chunk.size < size // continue whileSelect if chunk is not full + } + } + } catch (e: ClosedReceiveChannelException) { + Timber.e(e) + return@produce + } finally { + ticker.cancel() + if (chunk.isNotEmpty()) { + send(chunk.toList()) + } + } + } + } + +@OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class) +fun Flow.chunked(size: Int, time: Duration) = channelFlow { + coroutineScope { + val channel = asChannel(this@chunked).chunked(this, size, time) + try { + while (!channel.isClosedForReceive) { + send(channel.receive()) + } + } catch (e: ClosedReceiveChannelException) { + // Channel was closed by the flow completing, nothing to do + Timber.w(e) + } catch (e: CancellationException) { + channel.cancel(e) + throw e + } catch (e: Exception) { + channel.cancel(CancellationException("Closing channel due to flow exception", e)) + throw e + } + } +} + +@ExperimentalCoroutinesApi +fun CoroutineScope.asChannel(flow: Flow): ReceiveChannel = produce { + flow.collect { value -> + channel.send(value) + } +} + + + diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/FileUtils.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/FileUtils.kt index b4e6905..574741d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/FileUtils.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/FileUtils.kt @@ -6,20 +6,102 @@ import android.os.Build import android.os.Environment import android.provider.MediaStore import android.provider.MediaStore.MediaColumns +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext import timber.log.Timber import java.io.File +import java.io.FileInputStream import java.io.FileOutputStream import java.io.OutputStream import java.time.Instant import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream -object FileUtils { - private const val ZIP_FILE_MIME_TYPE = "application/zip" +class FileUtils( + private val context: Context, + private val ioDispatcher: CoroutineDispatcher, +) { + + suspend fun readBytesFromFile(file: File): ByteArray { + return withContext(ioDispatcher) { + FileInputStream(file).use { + it.readBytes() + } + } + } + + suspend fun readTextFromFileName(fileName: String): String { + return withContext(ioDispatcher) { + context.assets.open(fileName).use { stream -> + stream.bufferedReader(Charsets.UTF_8).use { + it.readText() + } + } + } + } + + suspend fun saveByteArrayToDownloads(content: ByteArray, fileName: String): Result { + return withContext(ioDispatcher) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val contentValues = + ContentValues().apply { + put(MediaColumns.DISPLAY_NAME, fileName) + put(MediaColumns.MIME_TYPE, Constants.TEXT_MIME_TYPE) + put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) + } + val resolver = context.contentResolver + val uri = + resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) + if (uri != null) { + resolver.openOutputStream(uri).use { output -> + output?.write(content) + } + } + } else { + val target = + File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), + fileName, + ) + FileOutputStream(target).use { output -> + output.write(content) + } + } + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } + } + } + + suspend fun saveFilesToZip(files: List): Result { + return withContext(ioDispatcher) { + try { + val zipOutputStream = + createDownloadsFileOutputStream( + "wg-export_${Instant.now().epochSecond}.zip", + Constants.ZIP_FILE_MIME_TYPE, + ) + ZipOutputStream(zipOutputStream).use { zos -> + files.forEach { file -> + val entry = ZipEntry(file.name) + zos.putNextEntry(entry) + if (file.isFile) { + file.inputStream().use { fis -> fis.copyTo(zos) } + } + } + return@withContext Result.success(Unit) + } + } catch (e: Exception) { + Timber.e(e) + Result.failure(WgTunnelExceptions.ConfigExportFailed()) + } + } + } //TODO issue with android 9 private fun createDownloadsFileOutputStream( - context: Context, fileName: String, mimeType: String = Constants.ALLOWED_FILE_TYPES ): OutputStream? { @@ -45,53 +127,4 @@ object FileUtils { } return null } - - fun saveFileToDownloads(context: Context, content: String, fileName: String) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - val contentValues = ContentValues().apply { - put(MediaColumns.DISPLAY_NAME, fileName) - put(MediaColumns.MIME_TYPE, Constants.TEXT_MIME_TYPE) - put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) - } - val resolver = context.contentResolver - val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) - if (uri != null) { - resolver.openOutputStream(uri).use { output -> - output?.write(content.toByteArray()) - } - } - } else { - val target = File( - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), - fileName, - ) - FileOutputStream(target).use { output -> - output.write(content.toByteArray()) - } - } - } - - fun saveFilesToZip(context: Context, files: List) : Result { - return try { - 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) - zos.putNextEntry(entry) - if (file.isFile) { - file.inputStream().use { fis -> fis.copyTo(zos) } - } - } - return Result.success(Unit) - } - } catch (e : Exception) { - Timber.e(e) - Result.failure(WgTunnelExceptions.ConfigExportFailed()) - } - } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/WgTunnelExceptions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/WgTunnelExceptions.kt index 9a9f662..9019398 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/WgTunnelExceptions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/WgTunnelExceptions.kt @@ -4,121 +4,127 @@ import android.content.Context import com.zaneschepke.wireguardautotunnel.R sealed class WgTunnelExceptions : Exception() { - abstract fun getMessage(context: Context) : String - data class General(private val userMessage : StringValue) : WgTunnelExceptions() { - override fun getMessage(context: Context) : String { + abstract fun getMessage(context: Context): String + data class General(private val userMessage: StringValue) : WgTunnelExceptions() { + override fun getMessage(context: Context): String { return userMessage.asString(context) } } - data class SsidConflict(private val userMessage : StringValue = StringValue.StringResource(R.string.error_ssid_exists)) : WgTunnelExceptions() { - override fun getMessage(context: Context) : String { + data class SsidConflict(private val userMessage: StringValue = StringValue.StringResource(R.string.error_ssid_exists)) : + WgTunnelExceptions() { + override fun getMessage(context: Context): String { return userMessage.asString(context) } } - data class ConfigExportFailed(private val userMessage : StringValue = StringValue.StringResource(R.string.export_configs_failed)) : WgTunnelExceptions() { - override fun getMessage(context: Context) : String { + data class ConfigExportFailed( + private val userMessage: StringValue = StringValue.StringResource( + R.string.export_configs_failed, + ) + ) : WgTunnelExceptions() { + override fun getMessage(context: Context): String { return userMessage.asString(context) } } - data class ConfigParseError(private val appendMessage : StringValue = StringValue.Empty) : WgTunnelExceptions() { - override fun getMessage(context: Context) : String { + data class ConfigParseError(private val appendMessage: StringValue = StringValue.Empty) : + WgTunnelExceptions() { + override fun getMessage(context: Context): String { return StringValue.StringResource(R.string.config_parse_error).asString(context) + ( if (appendMessage != StringValue.Empty) ": ${appendMessage.asString(context)}" else "") } } - data class RootDenied(private val userMessage : StringValue = StringValue.StringResource(R.string.error_root_denied)) : WgTunnelExceptions() { - override fun getMessage(context: Context) : String { + data class RootDenied(private val userMessage: StringValue = StringValue.StringResource(R.string.error_root_denied)) : + WgTunnelExceptions() { + override fun getMessage(context: Context): String { return userMessage.asString(context) } } - data class InvalidQrCode(private val userMessage : StringValue = StringValue.StringResource(R.string.error_invalid_code)) : WgTunnelExceptions() { - override fun getMessage(context: Context) : String { + data class InvalidQrCode(private val userMessage: StringValue = StringValue.StringResource(R.string.error_invalid_code)) : + WgTunnelExceptions() { + override fun getMessage(context: Context): String { return userMessage.asString(context) } } - data class InvalidFileExtension(private val userMessage : StringValue = StringValue.StringResource(R.string.error_file_extension)) : WgTunnelExceptions() { - override fun getMessage(context: Context) : String { + data class InvalidFileExtension( + private val userMessage: StringValue = StringValue.StringResource( + R.string.error_file_extension, + ) + ) : WgTunnelExceptions() { + override fun getMessage(context: Context): String { return userMessage.asString(context) } } - data class FileReadFailed(private val userMessage : StringValue = StringValue.StringResource(R.string.error_file_format)) : WgTunnelExceptions() { - override fun getMessage(context: Context) : String { + data class FileReadFailed(private val userMessage: StringValue = StringValue.StringResource(R.string.error_file_format)) : + WgTunnelExceptions() { + override fun getMessage(context: Context): String { return userMessage.asString(context) } } - data class AuthenticationFailed(private val userMessage : StringValue = StringValue.StringResource(R.string.error_authentication_failed)) : WgTunnelExceptions() { - override fun getMessage(context: Context) : String { + data class AuthenticationFailed( + private val userMessage: StringValue = StringValue.StringResource( + R.string.error_authentication_failed, + ) + ) : WgTunnelExceptions() { + override fun getMessage(context: Context): String { return userMessage.asString(context) } } - data class AuthorizationFailed(private val userMessage : StringValue = StringValue.StringResource(R.string.error_authorization_failed)) : WgTunnelExceptions() { - override fun getMessage(context: Context) : String { + data class AuthorizationFailed( + private val userMessage: StringValue = StringValue.StringResource( + R.string.error_authorization_failed, + ) + ) : WgTunnelExceptions() { + override fun getMessage(context: Context): String { return userMessage.asString(context) } } - data class BackgroundLocationRequired(private val userMessage : StringValue = StringValue.StringResource(R.string.background_location_required)) : WgTunnelExceptions() { - override fun getMessage(context: Context) : String { + data class BackgroundLocationRequired( + private val userMessage: StringValue = StringValue.StringResource( + R.string.background_location_required, + ) + ) : WgTunnelExceptions() { + override fun getMessage(context: Context): String { return userMessage.asString(context) } } - data class LocationServicesRequired(private val userMessage : StringValue = StringValue.StringResource(R.string.location_services_required)) : WgTunnelExceptions() { - override fun getMessage(context: Context) : String { + data class LocationServicesRequired( + private val userMessage: StringValue = StringValue.StringResource( + R.string.location_services_required, + ) + ) : WgTunnelExceptions() { + override fun getMessage(context: Context): String { return userMessage.asString(context) } } - data class PreciseLocationRequired(private val userMessage : StringValue = StringValue.StringResource(R.string.precise_location_required)) : WgTunnelExceptions() { - override fun getMessage(context: Context) : String { + data class PreciseLocationRequired( + private val userMessage: StringValue = StringValue.StringResource( + R.string.precise_location_required, + ) + ) : WgTunnelExceptions() { + override fun getMessage(context: Context): String { return userMessage.asString(context) } } - data class FileExplorerRequired (private val userMessage : StringValue = StringValue.StringResource(R.string.error_no_file_explorer)) : WgTunnelExceptions() { - override fun getMessage(context: Context) : String { + data class FileExplorerRequired( + private val userMessage: StringValue = StringValue.StringResource( + R.string.error_no_file_explorer, + ) + ) : WgTunnelExceptions() { + override fun getMessage(context: Context): String { return userMessage.asString(context) } } - - - - - -// sealed class Message : Event() { -// data object ConfigSaved : Message() { -// override val message: String -// get() = WireGuardAutoTunnel.instance.getString(R.string.config_changes_saved) -// } -// -// data object ConfigsExported : Message() { -// override val message: String -// get() = WireGuardAutoTunnel.instance.getString(R.string.exported_configs_message) -// } -// -// data object TunnelOffAction : Message() { -// override val message: String -// get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_tunnel) -// } -// -// data object TunnelOnAction : Message() { -// override val message: String -// get() = WireGuardAutoTunnel.instance.getString(R.string.turn_on_tunnel) -// } -// -// data object AutoTunnelOffAction : Message() { -// override val message: String -// get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_auto) -// } -// } - } +} diff --git a/app/src/main/res/drawable/add.xml b/app/src/main/res/drawable/add.xml index e4bb397..1402252 100644 --- a/app/src/main/res/drawable/add.xml +++ b/app/src/main/res/drawable/add.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960"> - + diff --git a/app/src/main/res/drawable/close.xml b/app/src/main/res/drawable/close.xml index 0efa44f..29d2cc1 100644 --- a/app/src/main/res/drawable/close.xml +++ b/app/src/main/res/drawable/close.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960"> - + diff --git a/app/src/main/res/drawable/edit.xml b/app/src/main/res/drawable/edit.xml index a2f714d..460708a 100644 --- a/app/src/main/res/drawable/edit.xml +++ b/app/src/main/res/drawable/edit.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960"> - + diff --git a/app/src/main/res/drawable/telegram.xml b/app/src/main/res/drawable/telegram.xml index 58f7407..9a89e02 100644 --- a/app/src/main/res/drawable/telegram.xml +++ b/app/src/main/res/drawable/telegram.xml @@ -3,7 +3,7 @@ android:height="50dp" android:viewportWidth="50" android:viewportHeight="50"> - + diff --git a/buildSrc/src/main/kotlin/Constants.kt b/buildSrc/src/main/kotlin/Constants.kt index 81454bd..aa2dcf0 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.4.4" + const val VERSION_NAME = "3.4.5" const val JVM_TARGET = "17" - const val VERSION_CODE = 34400 + const val VERSION_CODE = 34500 const val TARGET_SDK = 34 const val MIN_SDK = 26 const val APP_ID = "com.zaneschepke.wireguardautotunnel" diff --git a/fastlane/metadata/android/en-US/changelogs/34500.txt b/fastlane/metadata/android/en-US/changelogs/34500.txt new file mode 100644 index 0000000..f463fcf --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/34500.txt @@ -0,0 +1,5 @@ +What's new: +- Additional language support +- Auto-tunneling mobile data bug fix +- AndroidTV floating action button fix +- Other optimizations and enhancements \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bf4e4f7..93d8ceb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,19 +16,18 @@ junit = "4.13.2" kotlinx-serialization-json = "1.6.3" lifecycle-runtime-compose = "2.7.0" material3 = "1.2.1" -multifabVersion = "1.0.9" +multifabVersion = "1.1.0" navigationCompose = "2.7.7" pinLockCompose = "1.0.3" roomVersion = "2.6.1" timber = "5.0.1" tunnel = "1.0.20230706" -androidGradlePlugin = "8.4.0" +androidGradlePlugin = "8.4.1" kotlin = "1.9.23" ksp = "1.9.23-1.0.19" composeBom = "2024.05.00" compose = "1.6.7" zxingAndroidEmbedded = "4.3.0" -zxingCore = "3.5.3" #plugins gradlePlugins-kotlinxSerialization = "1.9.23" @@ -89,7 +88,6 @@ timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } tunnel = { module = "com.wireguard.android:tunnel", version.ref = "tunnel" } zaneschepke-multifab = { module = "com.zaneschepke:multifab", version.ref = "multifabVersion" } -zxing-core = { module = "com.google.zxing:core", version.ref = "zxingCore" } zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxingAndroidEmbedded" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 34bae8c..c342c96 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ #Wed Oct 11 22:39:21 EDT 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip distributionSha256Sum=9631d53cf3e74bfa726893aee1f8994fee4e060c401335946dba2156f440f24c validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/logcatter/build.gradle.kts b/logcatter/build.gradle.kts index bbe4e01..50e9b17 100644 --- a/logcatter/build.gradle.kts +++ b/logcatter/build.gradle.kts @@ -40,4 +40,7 @@ dependencies { testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) + + // logging + implementation(libs.timber) } diff --git a/logcatter/src/main/java/com/zaneschepke/logcatter/LocalLogCollector.kt b/logcatter/src/main/java/com/zaneschepke/logcatter/LocalLogCollector.kt new file mode 100644 index 0000000..f0de200 --- /dev/null +++ b/logcatter/src/main/java/com/zaneschepke/logcatter/LocalLogCollector.kt @@ -0,0 +1,13 @@ +package com.zaneschepke.logcatter + +import com.zaneschepke.logcatter.model.LogMessage +import kotlinx.coroutines.flow.Flow +import java.io.File + +interface LocalLogCollector { + fun start(onLogMessage: ((message: LogMessage) -> Unit)? = null) + fun stop() + suspend fun getLogFile(): Result + val bufferedLogs: Flow +} + diff --git a/logcatter/src/main/java/com/zaneschepke/logcatter/Logcatter.kt b/logcatter/src/main/java/com/zaneschepke/logcatter/Logcatter.kt index f1fd5ee..8496d40 100644 --- a/logcatter/src/main/java/com/zaneschepke/logcatter/Logcatter.kt +++ b/logcatter/src/main/java/com/zaneschepke/logcatter/Logcatter.kt @@ -1,34 +1,271 @@ package com.zaneschepke.logcatter +import android.content.Context +import com.zaneschepke.logcatter.model.LogLevel import com.zaneschepke.logcatter.model.LogMessage +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.io.BufferedReader +import java.io.File +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStreamReader +import java.nio.file.Files +import java.nio.file.Paths +import java.nio.file.StandardOpenOption -object Logcatter { +object LogcatHelper { + private const val MAX_FILE_SIZE = 2097152L // 2MB + private const val MAX_FOLDER_SIZE = 10485760L // 10MB private val findKeyRegex = """[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=""".toRegex() - private val findIpv6AddressRegex = """(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))""".toRegex() + private val findIpv6AddressRegex = + """(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))""".toRegex() private val findIpv4AddressRegex = """((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}""".toRegex() private val findTunnelNameRegex = """(?<=tunnel ).*?(?= UP| DOWN)""".toRegex() + private const val CHORE = "Choreographer" + private object LogcatHelperInit { + var maxFileSize: Long = MAX_FILE_SIZE + var maxFolderSize: Long = MAX_FOLDER_SIZE + var pID: Int = 0 + var publicAppDirectory = "" + var logcatPath = "" + } - fun logs(callback: (input: LogMessage) -> Unit, obfuscator: (log : String) -> String = { log -> this.obfuscator(log)}){ - clear() - Runtime.getRuntime().exec("logcat -v epoch") - .inputStream - .bufferedReader() - .useLines { lines -> - lines.forEach { callback(LogMessage.from(obfuscator(it))) } + fun init( + maxFileSize: Long = MAX_FILE_SIZE, + maxFolderSize: Long = MAX_FOLDER_SIZE, + context: Context + ): LocalLogCollector { + if (maxFileSize > maxFolderSize) { + throw IllegalStateException("maxFileSize must be less than maxFolderSize") + } + synchronized(LogcatHelperInit) { + LogcatHelperInit.maxFileSize = maxFileSize + LogcatHelperInit.maxFolderSize = maxFolderSize + LogcatHelperInit.pID = android.os.Process.myPid() + context.getExternalFilesDir(null)?.let { + LogcatHelperInit.publicAppDirectory = it.absolutePath + LogcatHelperInit.logcatPath = + LogcatHelperInit.publicAppDirectory + File.separator + "logs" + val logDirectory = File(LogcatHelperInit.logcatPath) + if (!logDirectory.exists()) { + logDirectory.mkdir() + } + } + return Logcat } } - private fun obfuscator(log : String) : String { - return findKeyRegex.replace(log, "").let { first -> - findIpv6AddressRegex.replace(first, "").let { second -> - findTunnelNameRegex.replace(second, "") - } - }.let{ last -> findIpv4AddressRegex.replace(last,"") } - } + internal object Logcat : LocalLogCollector { - fun clear() { - Runtime.getRuntime().exec("logcat -c") + private var logcatReader: LogcatReader? = null + + override fun start(onLogMessage: ((message: LogMessage) -> Unit)?) { + logcatReader ?: run { + logcatReader = LogcatReader( + LogcatHelperInit.pID.toString(), + LogcatHelperInit.logcatPath, + onLogMessage, + ) + } + logcatReader?.let { logReader -> + if (!logReader.isAlive) logReader.start() + } + } + + override fun stop() { + logcatReader?.stopLogs() + logcatReader = null + } + + private fun mergeLogsApi26(sourceDir: String, outputFile: File) { + val outputFilePath = Paths.get(outputFile.absolutePath) + val logcatPath = Paths.get(sourceDir) + + Files.list(logcatPath).use { + it.sorted { o1, o2 -> + Files.getLastModifiedTime(o1).compareTo(Files.getLastModifiedTime(o2)) + } + .flatMap(Files::lines).use { lines -> + lines.forEach { line -> + Files.write( + outputFilePath, + (line + System.lineSeparator()).toByteArray(), + StandardOpenOption.CREATE, + StandardOpenOption.APPEND, + ) + } + } + } + } + + override suspend fun getLogFile(): Result { + stop() + return withContext(Dispatchers.IO) { + try { + val outputDir = + File(LogcatHelperInit.publicAppDirectory + File.separator + "output") + val outputFile = File(outputDir.absolutePath + File.separator + "logs.txt") + + if (!outputDir.exists()) outputDir.mkdir() + if (outputFile.exists()) outputFile.delete() + + mergeLogsApi26(LogcatHelperInit.logcatPath, outputFile) + Result.success(outputFile) + } catch (e: Exception) { + Result.failure(e) + } finally { + start() + } + } + } + + private val _bufferedLogs = MutableSharedFlow( + replay = 10_000, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + + override val bufferedLogs: Flow = _bufferedLogs.asSharedFlow() + + private class LogcatReader( + pID: String, + private val logcatPath: String, + private val callback: ((input: LogMessage) -> Unit)?, + ) : Thread() { + private var logcatProc: Process? = null + private var reader: BufferedReader? = null + private var mRunning = true + private var command = "" + private var clearLogCommand = "" + private var outputStream: FileOutputStream? = null + + init { + try { + outputStream = FileOutputStream(createLogFile(logcatPath)) + } catch (e: FileNotFoundException) { + Timber.e(e) + } + + command = "logcat -v epoch | grep \"($pID)\"" + clearLogCommand = "logcat -c" + } + + fun stopLogs() { + mRunning = false + } + + fun clear() { + Runtime.getRuntime().exec(clearLogCommand) + } + + private fun obfuscator(log: String): String { + return findKeyRegex.replace(log, "").let { first -> + findIpv6AddressRegex.replace(first, "").let { second -> + findTunnelNameRegex.replace(second, "") + } + }.let { last -> findIpv4AddressRegex.replace(last, "") } + } + + override fun run() { + if (outputStream == null) return + try { + clear() + logcatProc = Runtime.getRuntime().exec(command) + reader = BufferedReader(InputStreamReader(logcatProc!!.inputStream), 1024) + var line: String? = null + + while (mRunning && run { + line = reader!!.readLine() + line + } != null + ) { + if (!mRunning) { + break + } + if (line!!.isEmpty()) { + continue + } + + if (outputStream!!.channel.size() >= LogcatHelperInit.maxFileSize) { + outputStream!!.close() + outputStream = FileOutputStream(createLogFile(logcatPath)) + } + if (getFolderSize(logcatPath) >= LogcatHelperInit.maxFolderSize) { + deleteOldestFile(logcatPath) + } + line?.let { text -> + val obfuscated = obfuscator(text) + outputStream!!.write((obfuscated + System.lineSeparator()).toByteArray()) + try { + val logMessage = LogMessage.from(obfuscated) + when (logMessage.level) { + LogLevel.VERBOSE -> Unit + else -> { + if (!logMessage.tag.contains(CHORE)) { + _bufferedLogs.tryEmit(logMessage) + } + } + } + callback?.let { + it(logMessage) + } + } catch (e: Exception) { + Timber.e(e) + } + } + } + } catch (e: IOException) { + Timber.e(e) + } finally { + logcatProc?.destroy() + logcatProc = null + + try { + reader?.close() + outputStream?.close() + reader = null + outputStream = null + } catch (e: IOException) { + Timber.e(e) + } + } + } + + private fun getFolderSize(path: String): Long { + File(path).run { + var size = 0L + if (this.isDirectory && this.listFiles() != null) { + for (file in this.listFiles()!!) { + size += getFolderSize(file.absolutePath) + } + } else { + size = this.length() + } + return size + } + } + + private fun createLogFile(dir: String): File { + return File(dir, "logcat_" + System.currentTimeMillis() + ".txt") + } + + private fun deleteOldestFile(path: String) { + val directory = File(path) + if (directory.isDirectory) { + directory.listFiles()?.toMutableList()?.run { + this.sortBy { it.lastModified() } + this.first().delete() + } + } + } + } } } diff --git a/logcatter/src/main/java/com/zaneschepke/logcatter/model/LogMessage.kt b/logcatter/src/main/java/com/zaneschepke/logcatter/model/LogMessage.kt index 90ef507..5b39018 100644 --- a/logcatter/src/main/java/com/zaneschepke/logcatter/model/LogMessage.kt +++ b/logcatter/src/main/java/com/zaneschepke/logcatter/model/LogMessage.kt @@ -3,12 +3,12 @@ package com.zaneschepke.logcatter.model import java.time.Instant data class LogMessage( - val time: Instant, + val time: String, val pid: String, val tid: String, val level: LogLevel, val tag: String, - val message: String + val message: String, ) { override fun toString(): String { return "$time $pid $tid $level $tag message= $message" @@ -16,21 +16,22 @@ data class LogMessage( companion object { fun from(logcatLine: String): LogMessage { - return if (logcatLine.contains("---------")) LogMessage( - Instant.now(), - "0", - "0", - LogLevel.VERBOSE, - "System", - logcatLine, - ) - else { - //TODO improve this + return if (logcatLine.contains("---------")) { + LogMessage( + Instant.now().toString(), + "0", + "0", + LogLevel.VERBOSE, + "System", + logcatLine, + ) + } else { + // TODO improve this val parts = logcatLine.trim().split(" ").filter { it.isNotEmpty() } val epochParts = parts[0].split(".").map { it.toLong() } val message = parts.subList(5, parts.size).joinToString(" ") LogMessage( - Instant.ofEpochSecond(epochParts[0], epochParts[1]), + Instant.ofEpochSecond(epochParts[0], epochParts[1]).toString(), parts[1], parts[2], LogLevel.fromSignifier(parts[3]),