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