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]),