fix: mobile data tunneling
Fixes a bug where mobile data tunneling was not working properly in certain scenarios. Fixes an issue where the new floating action button was not working correctly on AndroidTV. Improved local logging. Additional refactors and optimizations.
This commit is contained in:
parent
57bb3f5e74
commit
54d9653f04
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
We as individuals involved in this project, pledge to participate in this
|
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
|
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
|
## Standard
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ name: Issue Updates Workflow
|
||||||
|
|
||||||
on:
|
on:
|
||||||
issues:
|
issues:
|
||||||
types: [opened, closed, reopened]
|
types: [ opened, closed, reopened ]
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
|
@ -2,7 +2,7 @@ name: Release Updates Workflow
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [ published ]
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
16
README.md
16
README.md
|
@ -22,7 +22,8 @@ WG Tunnel
|
||||||
|
|
||||||
<div align="left">
|
<div align="left">
|
||||||
|
|
||||||
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)
|
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
|
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.
|
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
|
## 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
|
## 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/)
|
[![Translation status](https://hosted.weblate.org/widgets/wg-tunnel/-/multi-auto.svg)](https://hosted.weblate.org/engage/wg-tunnel/)
|
||||||
|
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
@ -203,7 +203,6 @@ dependencies {
|
||||||
|
|
||||||
// barcode scanning
|
// barcode scanning
|
||||||
implementation(libs.zxing.android.embedded)
|
implementation(libs.zxing.android.embedded)
|
||||||
implementation(libs.zxing.core)
|
|
||||||
|
|
||||||
// bio
|
// bio
|
||||||
implementation(libs.androidx.biometric.ktx)
|
implementation(libs.androidx.biometric.ktx)
|
||||||
|
|
|
@ -2,4 +2,4 @@
|
||||||
|
|
||||||
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||||
<fields>;
|
<fields>;
|
||||||
}
|
}
|
|
@ -21,6 +21,4 @@
|
||||||
#-renamesourcefileattribute SourceFile
|
#-renamesourcefileattribute SourceFile
|
||||||
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||||
<fields>;
|
<fields>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,35 +3,59 @@ package com.zaneschepke.wireguardautotunnel
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.StrictMode
|
||||||
|
import android.os.StrictMode.ThreadPolicy
|
||||||
import android.service.quicksettings.TileService
|
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.AutoTunnelControlTile
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
||||||
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class WireGuardAutoTunnel : Application() {
|
class WireGuardAutoTunnel : Application() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var localLogCollector: LocalLogCollector
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@ApplicationScope
|
||||||
|
lateinit var applicationScope: CoroutineScope
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@IoDispatcher
|
||||||
|
lateinit var ioDispatcher: CoroutineDispatcher
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
instance = this
|
instance = this
|
||||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) else Timber.plant(ReleaseTree())
|
if (BuildConfig.DEBUG) {
|
||||||
PinManager.initialize(this)
|
Timber.plant(Timber.DebugTree())
|
||||||
}
|
StrictMode.setThreadPolicy(
|
||||||
|
ThreadPolicy.Builder()
|
||||||
override fun onLowMemory() {
|
.detectDiskReads()
|
||||||
super.onLowMemory()
|
.detectDiskWrites()
|
||||||
applicationScope.cancel("onLowMemory() called by system")
|
.detectNetwork()
|
||||||
applicationScope = MainScope()
|
.penaltyLog()
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
} else Timber.plant(ReleaseTree())
|
||||||
|
applicationScope.launch(ioDispatcher) {
|
||||||
|
PinManager.initialize(this@WireGuardAutoTunnel)
|
||||||
|
if (!isRunningOnAndroidTv()) localLogCollector.start()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
var applicationScope = MainScope()
|
|
||||||
|
|
||||||
lateinit var instance: WireGuardAutoTunnel
|
lateinit var instance: WireGuardAutoTunnel
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||||
to = 7,
|
to = 7,
|
||||||
spec = RemoveLegacySettingColumnsMigration::class,
|
spec = RemoveLegacySettingColumnsMigration::class,
|
||||||
),
|
),
|
||||||
AutoMigration(7, 8)
|
AutoMigration(7, 8),
|
||||||
],
|
],
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
)
|
)
|
||||||
|
|
|
@ -21,7 +21,7 @@ interface TunnelConfigDao {
|
||||||
suspend fun getById(id: Long): TunnelConfig?
|
suspend fun getById(id: Long): TunnelConfig?
|
||||||
|
|
||||||
@Query("SELECT * FROM TunnelConfig WHERE name=:name")
|
@Query("SELECT * FROM TunnelConfig WHERE name=:name")
|
||||||
suspend fun getByName(name: String) : TunnelConfig?
|
suspend fun getByName(name: String): TunnelConfig?
|
||||||
|
|
||||||
@Query("SELECT * FROM TunnelConfig")
|
@Query("SELECT * FROM TunnelConfig")
|
||||||
suspend fun getAll(): TunnelConfigs
|
suspend fun getAll(): TunnelConfigs
|
||||||
|
@ -36,10 +36,10 @@ interface TunnelConfigDao {
|
||||||
suspend fun findByTunnelNetworkName(name: String): TunnelConfigs
|
suspend fun findByTunnelNetworkName(name: String): TunnelConfigs
|
||||||
|
|
||||||
@Query("UPDATE TunnelConfig SET is_primary_tunnel = 0 WHERE is_primary_tunnel =1")
|
@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")
|
@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")
|
@Query("SELECT * FROM TUNNELCONFIG WHERE is_primary_tunnel=1")
|
||||||
suspend fun findByPrimary(): TunnelConfigs
|
suspend fun findByPrimary(): TunnelConfigs
|
||||||
|
|
|
@ -7,14 +7,20 @@ import androidx.datastore.preferences.core.edit
|
||||||
import androidx.datastore.preferences.core.intPreferencesKey
|
import androidx.datastore.preferences.core.intPreferencesKey
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
|
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class DataStoreManager(private val context: Context) {
|
class DataStoreManager(
|
||||||
|
private val context: Context,
|
||||||
|
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
||||||
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
|
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
|
||||||
val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
|
val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
|
||||||
|
@ -32,20 +38,24 @@ class DataStoreManager(private val context: Context) {
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun init() {
|
suspend fun init() {
|
||||||
try {
|
withContext(ioDispatcher) {
|
||||||
context.dataStore.data.first()
|
try {
|
||||||
} catch (e: IOException) {
|
context.dataStore.data.first()
|
||||||
Timber.e(e)
|
} catch (e: IOException) {
|
||||||
|
Timber.e(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) {
|
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) {
|
||||||
try {
|
withContext(ioDispatcher) {
|
||||||
context.dataStore.edit { it[key] = value }
|
try {
|
||||||
} catch (e: IOException) {
|
context.dataStore.edit { it[key] = value }
|
||||||
Timber.e(e)
|
} catch (e: IOException) {
|
||||||
} catch (e: Exception) {
|
Timber.e(e)
|
||||||
Timber.e(e)
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,11 +63,13 @@ class DataStoreManager(private val context: Context) {
|
||||||
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] }
|
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] }
|
||||||
|
|
||||||
suspend fun <T> getFromStore(key: Preferences.Key<T>): T? {
|
suspend fun <T> getFromStore(key: Preferences.Key<T>): T? {
|
||||||
return try {
|
return withContext(ioDispatcher) {
|
||||||
context.dataStore.data.map { it[key] }.first()
|
try {
|
||||||
} catch (e: IOException) {
|
context.dataStore.data.map { it[key] }.first()
|
||||||
Timber.e(e)
|
} catch (e: IOException) {
|
||||||
null
|
Timber.e(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,12 +40,14 @@ data class TunnelConfig(
|
||||||
Config.parse(it)
|
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()
|
val inputStream: InputStream = amQuick.byteInputStream()
|
||||||
return inputStream.bufferedReader(Charsets.UTF_8).use {
|
return inputStream.bufferedReader(Charsets.UTF_8).use {
|
||||||
org.amnezia.awg.config.Config.parse(it)
|
org.amnezia.awg.config.Config.parse(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const val AM_QUICK_DEFAULT = ""
|
const val AM_QUICK_DEFAULT = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ interface TunnelConfigRepository {
|
||||||
|
|
||||||
suspend fun count(): Int
|
suspend fun count(): Int
|
||||||
|
|
||||||
suspend fun findByTunnelName(name : String) : TunnelConfig?
|
suspend fun findByTunnelName(name: String): TunnelConfig?
|
||||||
|
|
||||||
suspend fun findByTunnelNetworksName(name: String): TunnelConfigs
|
suspend fun findByTunnelNetworksName(name: String): TunnelConfigs
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,10 @@ package com.zaneschepke.wireguardautotunnel.module
|
||||||
|
|
||||||
import javax.inject.Qualifier
|
import javax.inject.Qualifier
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@Retention(AnnotationRetention.BINARY)
|
||||||
|
annotation class Kernel
|
||||||
|
|
||||||
@Qualifier
|
@Qualifier
|
||||||
@Retention(AnnotationRetention.BINARY)
|
@Retention(AnnotationRetention.BINARY)
|
||||||
annotation class Userspace
|
annotation class Userspace
|
|
@ -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
|
|
@ -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
|
||||||
|
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.module
|
|
||||||
|
|
||||||
import javax.inject.Qualifier
|
|
||||||
|
|
||||||
@Qualifier
|
|
||||||
@Retention(AnnotationRetention.BINARY)
|
|
||||||
annotation class Kernel
|
|
|
@ -1,7 +1,10 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.module
|
package com.zaneschepke.wireguardautotunnel.module
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.room.Room
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
|
||||||
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
||||||
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
||||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||||
|
@ -18,11 +21,25 @@ import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
class RepositoryModule {
|
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
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun provideSettingsDoa(appDatabase: AppDatabase): SettingsDao {
|
fun provideSettingsDoa(appDatabase: AppDatabase): SettingsDao {
|
||||||
|
@ -49,8 +66,11 @@ class RepositoryModule {
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun providePreferencesDataStore(@ApplicationContext context: Context): DataStoreManager {
|
fun providePreferencesDataStore(
|
||||||
return DataStoreManager(context)
|
@ApplicationContext context: Context,
|
||||||
|
@IoDispatcher ioDispatcher: CoroutineDispatcher
|
||||||
|
): DataStoreManager {
|
||||||
|
return DataStoreManager(context, ioDispatcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
@ -15,6 +15,8 @@ import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
@ -42,7 +44,7 @@ class TunnelModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@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)
|
return org.amnezia.awg.backend.GoBackend(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,14 +54,26 @@ class TunnelModule {
|
||||||
amneziaBackend: org.amnezia.awg.backend.Backend,
|
amneziaBackend: org.amnezia.awg.backend.Backend,
|
||||||
@Userspace userspaceBackend: Backend,
|
@Userspace userspaceBackend: Backend,
|
||||||
@Kernel kernelBackend: Backend,
|
@Kernel kernelBackend: Backend,
|
||||||
appDataRepository: AppDataRepository
|
appDataRepository: AppDataRepository,
|
||||||
|
@ApplicationScope applicationScope: CoroutineScope,
|
||||||
|
@IoDispatcher ioDispatcher: CoroutineDispatcher
|
||||||
): VpnService {
|
): VpnService {
|
||||||
return WireGuardTunnel(amneziaBackend,userspaceBackend, kernelBackend, appDataRepository)
|
return WireGuardTunnel(
|
||||||
|
amneziaBackend,
|
||||||
|
userspaceBackend,
|
||||||
|
kernelBackend,
|
||||||
|
appDataRepository,
|
||||||
|
applicationScope,
|
||||||
|
ioDispatcher,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideServiceManager(appDataRepository: AppDataRepository): ServiceManager {
|
fun provideServiceManager(
|
||||||
return ServiceManager(appDataRepository)
|
appDataRepository: AppDataRepository,
|
||||||
|
@IoDispatcher ioDispatcher: CoroutineDispatcher
|
||||||
|
): ServiceManager {
|
||||||
|
return ServiceManager(appDataRepository, ioDispatcher)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,6 +24,7 @@ open class ForegroundService : LifecycleService() {
|
||||||
when (action) {
|
when (action) {
|
||||||
Action.START.name,
|
Action.START.name,
|
||||||
Action.START_FOREGROUND.name -> startService(intent.extras)
|
Action.START_FOREGROUND.name -> startService(intent.extras)
|
||||||
|
|
||||||
Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService()
|
Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService()
|
||||||
Constants.ALWAYS_ON_VPN_ACTION -> {
|
Constants.ALWAYS_ON_VPN_ACTION -> {
|
||||||
Timber.i("Always-on VPN starting service")
|
Timber.i("Always-on VPN starting service")
|
||||||
|
|
|
@ -4,10 +4,16 @@ import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class ServiceManager(private val appDataRepository: AppDataRepository) {
|
class ServiceManager(
|
||||||
|
private val appDataRepository: AppDataRepository,
|
||||||
|
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
||||||
|
) {
|
||||||
|
|
||||||
private fun <T : Service> actionOnService(
|
private fun <T : Service> actionOnService(
|
||||||
action: Action,
|
action: Action,
|
||||||
|
@ -23,7 +29,10 @@ class ServiceManager(private val appDataRepository: AppDataRepository) {
|
||||||
intent.component?.javaClass
|
intent.component?.javaClass
|
||||||
try {
|
try {
|
||||||
when (action) {
|
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)
|
Action.START, Action.STOP -> context.startService(intent)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -46,23 +55,27 @@ class ServiceManager(private val appDataRepository: AppDataRepository) {
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun stopVpnServiceForeground(context: Context, isManualStop: Boolean = false) {
|
suspend fun stopVpnServiceForeground(context: Context, isManualStop: Boolean = false) {
|
||||||
if (isManualStop) onManualStop()
|
withContext(ioDispatcher) {
|
||||||
Timber.i("Stopping vpn service")
|
if (isManualStop) onManualStop()
|
||||||
actionOnService(
|
Timber.i("Stopping vpn service")
|
||||||
Action.STOP_FOREGROUND,
|
actionOnService(
|
||||||
context,
|
Action.STOP_FOREGROUND,
|
||||||
WireGuardTunnelService::class.java,
|
context,
|
||||||
)
|
WireGuardTunnelService::class.java,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun stopVpnService(context: Context, isManualStop: Boolean = false) {
|
suspend fun stopVpnService(context: Context, isManualStop: Boolean = false) {
|
||||||
if (isManualStop) onManualStop()
|
withContext(ioDispatcher) {
|
||||||
Timber.i("Stopping vpn service")
|
if (isManualStop) onManualStop()
|
||||||
actionOnService(
|
Timber.i("Stopping vpn service")
|
||||||
Action.STOP,
|
actionOnService(
|
||||||
context,
|
Action.STOP,
|
||||||
WireGuardTunnelService::class.java,
|
context,
|
||||||
)
|
WireGuardTunnelService::class.java,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun onManualStop() {
|
private suspend fun onManualStop() {
|
||||||
|
@ -80,13 +93,15 @@ class ServiceManager(private val appDataRepository: AppDataRepository) {
|
||||||
tunnelId: Int? = null,
|
tunnelId: Int? = null,
|
||||||
isManualStart: Boolean = false
|
isManualStart: Boolean = false
|
||||||
) {
|
) {
|
||||||
if (isManualStart) onManualStart(tunnelId)
|
withContext(ioDispatcher) {
|
||||||
actionOnService(
|
if (isManualStart) onManualStart(tunnelId)
|
||||||
Action.START_FOREGROUND,
|
actionOnService(
|
||||||
context,
|
Action.START_FOREGROUND,
|
||||||
WireGuardTunnelService::class.java,
|
context,
|
||||||
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
|
WireGuardTunnelService::class.java,
|
||||||
)
|
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startWatcherServiceForeground(
|
fun startWatcherServiceForeground(
|
||||||
|
|
|
@ -21,13 +21,6 @@ data class WatcherState(
|
||||||
isMobileDataConnected)
|
isMobileDataConnected)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isTunnelOnMobileDataPreferredConditionMet(): Boolean {
|
|
||||||
return (!isEthernetConnected &&
|
|
||||||
settings.isTunnelOnMobileDataEnabled &&
|
|
||||||
!isWifiConnected &&
|
|
||||||
isMobileDataConnected)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isTunnelOffOnMobileDataConditionMet(): Boolean {
|
fun isTunnelOffOnMobileDataConditionMet(): Boolean {
|
||||||
return (!isEthernetConnected &&
|
return (!isEthernetConnected &&
|
||||||
!settings.isTunnelOnMobileDataEnabled &&
|
!settings.isTunnelOnMobileDataEnabled &&
|
||||||
|
|
|
@ -8,6 +8,8 @@ import androidx.lifecycle.lifecycleScope
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
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.EthernetService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
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 com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -56,6 +59,14 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var serviceManager: ServiceManager
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@IoDispatcher
|
||||||
|
lateinit var ioDispatcher: CoroutineDispatcher
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@MainImmediateDispatcher
|
||||||
|
lateinit var mainImmediateDispatcher: CoroutineDispatcher
|
||||||
|
|
||||||
private val networkEventsFlow = MutableStateFlow(WatcherState())
|
private val networkEventsFlow = MutableStateFlow(WatcherState())
|
||||||
|
|
||||||
private var watcherJob: Job? = null
|
private var watcherJob: Job? = null
|
||||||
|
@ -65,7 +76,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(mainImmediateDispatcher) {
|
||||||
try {
|
try {
|
||||||
if (appDataRepository.settings.getSettings().isAutoTunnelPaused) {
|
if (appDataRepository.settings.getSettings().isAutoTunnelPaused) {
|
||||||
launchWatcherPausedNotification()
|
launchWatcherPausedNotification()
|
||||||
|
@ -138,14 +149,14 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
private fun cancelWatcherJob() {
|
private fun cancelWatcherJob() {
|
||||||
try {
|
try {
|
||||||
watcherJob?.cancel()
|
watcherJob?.cancel()
|
||||||
} catch (e : CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
Timber.i("Watcher job cancelled")
|
Timber.i("Watcher job cancelled")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startWatcherJob() {
|
private fun startWatcherJob() {
|
||||||
watcherJob =
|
watcherJob =
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch {
|
||||||
val setting = appDataRepository.settings.getSettings()
|
val setting = appDataRepository.settings.getSettings()
|
||||||
launch {
|
launch {
|
||||||
Timber.i("Starting wifi watcher")
|
Timber.i("Starting wifi watcher")
|
||||||
|
@ -182,69 +193,74 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun watchForMobileDataConnectivityChanges() {
|
private suspend fun watchForMobileDataConnectivityChanges() {
|
||||||
mobileDataService.networkStatus.collect { status ->
|
withContext(ioDispatcher) {
|
||||||
when (status) {
|
mobileDataService.networkStatus.collect { status ->
|
||||||
is NetworkStatus.Available -> {
|
when (status) {
|
||||||
Timber.i("Gained Mobile data connection")
|
is NetworkStatus.Available -> {
|
||||||
networkEventsFlow.update {
|
Timber.i("Gained Mobile data connection")
|
||||||
it.copy(
|
networkEventsFlow.update {
|
||||||
isMobileDataConnected = true,
|
it.copy(
|
||||||
)
|
isMobileDataConnected = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
networkEventsFlow.update {
|
networkEventsFlow.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isMobileDataConnected = true,
|
isMobileDataConnected = true,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
Timber.i("Mobile data capabilities changed")
|
||||||
}
|
}
|
||||||
Timber.i("Mobile data capabilities changed")
|
|
||||||
}
|
|
||||||
|
|
||||||
is NetworkStatus.Unavailable -> {
|
is NetworkStatus.Unavailable -> {
|
||||||
networkEventsFlow.update {
|
networkEventsFlow.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isMobileDataConnected = false,
|
isMobileDataConnected = false,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
Timber.i("Lost mobile data connection")
|
||||||
}
|
}
|
||||||
Timber.i("Lost mobile data connection")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun watchForPingFailure() {
|
private suspend fun watchForPingFailure() {
|
||||||
try {
|
val context = this
|
||||||
do {
|
withContext(ioDispatcher) {
|
||||||
if (vpnService.vpnState.value.status == TunnelState.UP) {
|
try {
|
||||||
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
|
do {
|
||||||
tunnelConfig?.let {
|
if (vpnService.vpnState.value.status == TunnelState.UP) {
|
||||||
val config = TunnelConfig.configFromWgQuick(it.wgQuick)
|
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
|
||||||
val results = config.peers.map { peer ->
|
tunnelConfig?.let {
|
||||||
val host = if (peer.endpoint.isPresent &&
|
val config = TunnelConfig.configFromWgQuick(it.wgQuick)
|
||||||
peer.endpoint.get().resolved.isPresent)
|
val results = config.peers.map { peer ->
|
||||||
peer.endpoint.get().resolved.get().host
|
val host = if (peer.endpoint.isPresent &&
|
||||||
else Constants.DEFAULT_PING_IP
|
peer.endpoint.get().resolved.isPresent)
|
||||||
Timber.i("Checking reachability of: $host")
|
peer.endpoint.get().resolved.get().host
|
||||||
val reachable = InetAddress.getByName(host)
|
else Constants.DEFAULT_PING_IP
|
||||||
.isReachable(Constants.PING_TIMEOUT.toInt())
|
Timber.i("Checking reachability of: $host")
|
||||||
Timber.i("Result: reachable - $reachable")
|
val reachable = InetAddress.getByName(host)
|
||||||
reachable
|
.isReachable(Constants.PING_TIMEOUT.toInt())
|
||||||
}
|
Timber.i("Result: reachable - $reachable")
|
||||||
if (results.contains(false)) {
|
reachable
|
||||||
Timber.i("Restarting VPN for ping failure")
|
}
|
||||||
serviceManager.stopVpnServiceForeground(this)
|
if (results.contains(false)) {
|
||||||
delay(Constants.VPN_RESTART_DELAY)
|
Timber.i("Restarting VPN for ping failure")
|
||||||
serviceManager.startVpnServiceForeground(this, it.id)
|
serviceManager.stopVpnServiceForeground(context)
|
||||||
delay(Constants.PING_COOLDOWN)
|
delay(Constants.VPN_RESTART_DELAY)
|
||||||
|
serviceManager.startVpnServiceForeground(context, it.id)
|
||||||
|
delay(Constants.PING_COOLDOWN)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
delay(Constants.PING_INTERVAL)
|
||||||
delay(Constants.PING_INTERVAL)
|
} while (true)
|
||||||
} while (true)
|
} catch (e: Exception) {
|
||||||
} catch (e: Exception) {
|
Timber.e(e)
|
||||||
Timber.e(e)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -265,77 +281,82 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun watchForEthernetConnectivityChanges() {
|
private suspend fun watchForEthernetConnectivityChanges() {
|
||||||
ethernetService.networkStatus.collect { status ->
|
withContext(ioDispatcher) {
|
||||||
when (status) {
|
ethernetService.networkStatus.collect { status ->
|
||||||
is NetworkStatus.Available -> {
|
when (status) {
|
||||||
Timber.i("Gained Ethernet connection")
|
is NetworkStatus.Available -> {
|
||||||
networkEventsFlow.update {
|
Timber.i("Gained Ethernet connection")
|
||||||
it.copy(
|
networkEventsFlow.update {
|
||||||
isEthernetConnected = true,
|
it.copy(
|
||||||
)
|
isEthernetConnected = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
Timber.i("Ethernet capabilities changed")
|
Timber.i("Ethernet capabilities changed")
|
||||||
networkEventsFlow.update {
|
networkEventsFlow.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isEthernetConnected = true,
|
isEthernetConnected = true,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is NetworkStatus.Unavailable -> {
|
is NetworkStatus.Unavailable -> {
|
||||||
networkEventsFlow.update {
|
networkEventsFlow.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isEthernetConnected = false,
|
isEthernetConnected = false,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
Timber.i("Lost Ethernet connection")
|
||||||
}
|
}
|
||||||
Timber.i("Lost Ethernet connection")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun watchForWifiConnectivityChanges() {
|
private suspend fun watchForWifiConnectivityChanges() {
|
||||||
wifiService.networkStatus.collect { status ->
|
withContext(ioDispatcher) {
|
||||||
when (status) {
|
wifiService.networkStatus.collect { status ->
|
||||||
is NetworkStatus.Available -> {
|
when (status) {
|
||||||
Timber.i("Gained Wi-Fi connection")
|
is NetworkStatus.Available -> {
|
||||||
networkEventsFlow.update {
|
Timber.i("Gained Wi-Fi connection")
|
||||||
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)
|
|
||||||
networkEventsFlow.update {
|
networkEventsFlow.update {
|
||||||
it.copy(
|
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()
|
return appDataRepository.tunnels.findByTunnelNetworksName(ssid).firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isTunnelDown() : Boolean {
|
private fun isTunnelDown(): Boolean {
|
||||||
return vpnService.vpnState.value.status == TunnelState.DOWN
|
return vpnService.vpnState.value.status == TunnelState.DOWN
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun manageVpn() {
|
private suspend fun manageVpn() {
|
||||||
networkEventsFlow.collectLatest { watcherState ->
|
val context = this
|
||||||
val autoTunnel = "Auto-tunnel watcher"
|
withContext(ioDispatcher) {
|
||||||
if (!watcherState.settings.isAutoTunnelPaused) {
|
networkEventsFlow.collectLatest { watcherState ->
|
||||||
//delay for rapid network state changes and then collect latest
|
val autoTunnel = "Auto-tunnel watcher"
|
||||||
delay(Constants.WATCHER_COLLECTION_DELAY)
|
if (!watcherState.settings.isAutoTunnelPaused) {
|
||||||
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
|
//delay for rapid network state changes and then collect latest
|
||||||
when {
|
delay(Constants.WATCHER_COLLECTION_DELAY)
|
||||||
watcherState.isEthernetConditionMet() -> {
|
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
|
||||||
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
|
when {
|
||||||
if(isTunnelDown()) serviceManager.startVpnServiceForeground(this)
|
watcherState.isEthernetConditionMet() -> {
|
||||||
}
|
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
|
||||||
|
if (isTunnelDown()) serviceManager.startVpnServiceForeground(context)
|
||||||
|
}
|
||||||
|
|
||||||
watcherState.isMobileDataConditionMet() -> {
|
watcherState.isMobileDataConditionMet() -> {
|
||||||
Timber.i("$autoTunnel - tunnel on on mobile data condition met")
|
Timber.i("$autoTunnel - tunnel on on mobile data condition met")
|
||||||
if(isTunnelDown()) serviceManager.startVpnServiceForeground(this, getMobileDataTunnel()?.id)
|
val mobileDataTunnel = getMobileDataTunnel()
|
||||||
}
|
val tunnel =
|
||||||
|
mobileDataTunnel ?: appDataRepository.getPrimaryOrFirstTunnel()
|
||||||
watcherState.isTunnelOnMobileDataPreferredConditionMet() -> {
|
if (isTunnelDown()) return@collectLatest serviceManager.startVpnServiceForeground(
|
||||||
if(tunnelConfig?.isMobileDataTunnel == false) {
|
context,
|
||||||
getMobileDataTunnel()?.let {
|
tunnel?.id,
|
||||||
|
)
|
||||||
|
if (tunnelConfig?.isMobileDataTunnel == false && mobileDataTunnel != null) {
|
||||||
Timber.i("$autoTunnel - tunnel connected on mobile data is not preferred condition met, switching to preferred")
|
Timber.i("$autoTunnel - tunnel connected on mobile data is not preferred condition met, switching to preferred")
|
||||||
if(isTunnelDown()) serviceManager.startVpnServiceForeground(
|
serviceManager.startVpnServiceForeground(
|
||||||
this,
|
context,
|
||||||
getMobileDataTunnel()?.id,
|
mobileDataTunnel.id,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
|
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
|
||||||
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
|
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
|
||||||
if(!isTunnelDown()) serviceManager.stopVpnServiceForeground(this)
|
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
|
||||||
}
|
|
||||||
|
|
||||||
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.isTrustedWifiConditionMet() -> {
|
watcherState.isUntrustedWifiConditionMet() -> {
|
||||||
Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off")
|
if (tunnelConfig?.tunnelNetworks?.contains(watcherState.currentNetworkSSID) == false ||
|
||||||
if(!isTunnelDown()) serviceManager.stopVpnServiceForeground(this)
|
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")
|
}.invoke()
|
||||||
if(!isTunnelDown()) serviceManager.stopVpnServiceForeground(this)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watcherState.isTunnelOffOnNoConnectivityMet() -> {
|
watcherState.isTrustedWifiConditionMet() -> {
|
||||||
Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off")
|
Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off")
|
||||||
if(!isTunnelDown()) serviceManager.stopVpnServiceForeground(this)
|
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
watcherState.isTunnelOffOnWifiConditionMet() -> {
|
||||||
Timber.i("$autoTunnel - no condition met")
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ import androidx.core.app.ServiceCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
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.receiver.NotificationActionReceiver
|
||||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||||
|
@ -17,10 +19,11 @@ import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -37,13 +40,21 @@ class WireGuardTunnelService : ForegroundService() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var notificationService: NotificationService
|
lateinit var notificationService: NotificationService
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@MainImmediateDispatcher
|
||||||
|
lateinit var mainImmediateDispatcher: CoroutineDispatcher
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@IoDispatcher
|
||||||
|
lateinit var ioDispatcher: CoroutineDispatcher
|
||||||
|
|
||||||
private var job: Job? = null
|
private var job: Job? = null
|
||||||
|
|
||||||
private var didShowConnected = false
|
private var didShowConnected = false
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(mainImmediateDispatcher) {
|
||||||
//TODO fix this to not launch if AOVPN
|
//TODO fix this to not launch if AOVPN
|
||||||
if (appDataRepository.tunnels.count() != 0) {
|
if (appDataRepository.tunnels.count() != 0) {
|
||||||
launchVpnNotification()
|
launchVpnNotification()
|
||||||
|
@ -55,7 +66,7 @@ class WireGuardTunnelService : ForegroundService() {
|
||||||
super.startService(extras)
|
super.startService(extras)
|
||||||
cancelJob()
|
cancelJob()
|
||||||
job =
|
job =
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch {
|
||||||
launch {
|
launch {
|
||||||
val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY)
|
val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY)
|
||||||
if (vpnService.getState() == TunnelState.UP) {
|
if (vpnService.getState() == TunnelState.UP) {
|
||||||
|
@ -75,39 +86,41 @@ class WireGuardTunnelService : ForegroundService() {
|
||||||
|
|
||||||
//TODO improve tunnel notifications
|
//TODO improve tunnel notifications
|
||||||
private suspend fun handshakeNotifications() {
|
private suspend fun handshakeNotifications() {
|
||||||
var tunnelName: String? = null
|
withContext(ioDispatcher) {
|
||||||
vpnService.vpnState.collect { state ->
|
var tunnelName: String? = null
|
||||||
|
vpnService.vpnState.collect { state ->
|
||||||
state.statistics
|
state.statistics
|
||||||
?.mapPeerStats()
|
?.mapPeerStats()
|
||||||
?.map { it.value?.handshakeStatus() }
|
?.map { it.value?.handshakeStatus() }
|
||||||
.let { statuses ->
|
.let { statuses ->
|
||||||
when {
|
when {
|
||||||
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
|
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
|
||||||
if (!didShowConnected) {
|
if (!didShowConnected) {
|
||||||
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
|
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
|
||||||
tunnelName = state.tunnelConfig?.name
|
tunnelName = state.tunnelConfig?.name
|
||||||
launchVpnNotification(
|
launchVpnNotification(
|
||||||
getString(R.string.tunnel_start_title),
|
getString(R.string.tunnel_start_title),
|
||||||
"${getString(R.string.tunnel_start_text)} - $tunnelName",
|
"${getString(R.string.tunnel_start_text)} - $tunnelName",
|
||||||
)
|
)
|
||||||
didShowConnected = true
|
didShowConnected = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
|
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
|
||||||
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
|
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
|
||||||
true -> {
|
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() {
|
override fun stopService() {
|
||||||
super.stopService()
|
super.stopService()
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch {
|
||||||
vpnService.stopTunnel()
|
vpnService.stopTunnel()
|
||||||
didShowConnected = false
|
didShowConnected = false
|
||||||
}
|
}
|
||||||
|
@ -181,7 +194,7 @@ class WireGuardTunnelService : ForegroundService() {
|
||||||
private fun cancelJob() {
|
private fun cancelJob() {
|
||||||
try {
|
try {
|
||||||
job?.cancel()
|
job?.cancel()
|
||||||
} catch (e : CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
Timber.i("Tunnel job cancelled")
|
Timber.i("Tunnel job cancelled")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,14 +2,14 @@ package com.zaneschepke.wireguardautotunnel.service.shortcut
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
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.Action
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
|
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -22,9 +22,13 @@ class ShortcutsActivity : ComponentActivity() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var serviceManager: ServiceManager
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@ApplicationScope
|
||||||
|
lateinit var applicationScope: CoroutineScope
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
WireGuardAutoTunnel.applicationScope.launch(Dispatchers.IO) {
|
applicationScope.launch {
|
||||||
val settings = appDataRepository.settings.getSettings()
|
val settings = appDataRepository.settings.getSettings()
|
||||||
if (settings.isShortcutsEnabled) {
|
if (settings.isShortcutsEnabled) {
|
||||||
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
|
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
|
||||||
|
|
|
@ -6,12 +6,11 @@ import android.service.quicksettings.TileService
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
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.foreground.ServiceManager
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -25,29 +24,30 @@ class AutoTunnelControlTile : TileService() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var serviceManager: ServiceManager
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO)
|
@Inject
|
||||||
|
@ApplicationScope
|
||||||
|
lateinit var applicationScope: CoroutineScope
|
||||||
|
|
||||||
private var manualStartConfig: TunnelConfig? = null
|
private var manualStartConfig: TunnelConfig? = null
|
||||||
|
|
||||||
override fun onStartListening() {
|
override fun onStartListening() {
|
||||||
super.onStartListening()
|
super.onStartListening()
|
||||||
scope.launch {
|
applicationScope.launch {
|
||||||
appDataRepository.settings.getSettingsFlow().collectLatest {
|
val settings = appDataRepository.settings.getSettings()
|
||||||
when (it.isAutoTunnelEnabled) {
|
when (settings.isAutoTunnelEnabled) {
|
||||||
true -> {
|
true -> {
|
||||||
if (it.isAutoTunnelPaused) {
|
if (settings.isAutoTunnelPaused) {
|
||||||
setInactive()
|
setInactive()
|
||||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.paused))
|
setTileDescription(this@AutoTunnelControlTile.getString(R.string.paused))
|
||||||
} else {
|
} else {
|
||||||
setActive()
|
setActive()
|
||||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.active))
|
setTileDescription(this@AutoTunnelControlTile.getString(R.string.active))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
false -> {
|
false -> {
|
||||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled))
|
setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled))
|
||||||
setUnavailable()
|
setUnavailable()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,20 +58,10 @@ class AutoTunnelControlTile : TileService() {
|
||||||
onStartListening()
|
onStartListening()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
scope.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTileRemoved() {
|
|
||||||
super.onTileRemoved()
|
|
||||||
scope.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
super.onClick()
|
super.onClick()
|
||||||
unlockAndRun {
|
unlockAndRun {
|
||||||
scope.launch {
|
applicationScope.launch {
|
||||||
try {
|
try {
|
||||||
appDataRepository.toggleWatcherServicePause()
|
appDataRepository.toggleWatcherServicePause()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
|
@ -5,12 +5,13 @@ import android.service.quicksettings.Tile
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
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.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -28,14 +29,19 @@ class TunnelControlTile : TileService() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var serviceManager: ServiceManager
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO)
|
@Inject
|
||||||
|
@ApplicationScope
|
||||||
|
lateinit var applicationScope: CoroutineScope
|
||||||
|
|
||||||
private var manualStartConfig: TunnelConfig? = null
|
private var manualStartConfig: TunnelConfig? = null
|
||||||
|
|
||||||
|
private var job: Job? = null;
|
||||||
|
|
||||||
override fun onStartListening() {
|
override fun onStartListening() {
|
||||||
super.onStartListening()
|
super.onStartListening()
|
||||||
Timber.d("On start listening called")
|
Timber.d("On start listening called")
|
||||||
scope.launch {
|
//TODO Fix this
|
||||||
|
if (job == null || job?.isCancelled == true) job = applicationScope.launch {
|
||||||
vpnService.vpnState.collect { it ->
|
vpnService.vpnState.collect { it ->
|
||||||
when (it.status) {
|
when (it.status) {
|
||||||
TunnelState.UP -> {
|
TunnelState.UP -> {
|
||||||
|
@ -52,22 +58,13 @@ class TunnelControlTile : TileService() {
|
||||||
setTileDescription(it.name)
|
setTileDescription(it.name)
|
||||||
} ?: setUnavailable()
|
} ?: setUnavailable()
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> setInactive()
|
else -> setInactive()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
scope.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTileRemoved() {
|
|
||||||
super.onTileRemoved()
|
|
||||||
scope.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTileAdded() {
|
override fun onTileAdded() {
|
||||||
super.onTileAdded()
|
super.onTileAdded()
|
||||||
onStartListening()
|
onStartListening()
|
||||||
|
@ -76,7 +73,7 @@ class TunnelControlTile : TileService() {
|
||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
super.onClick()
|
super.onClick()
|
||||||
unlockAndRun {
|
unlockAndRun {
|
||||||
scope.launch {
|
applicationScope.launch {
|
||||||
try {
|
try {
|
||||||
if (vpnService.getState() == TunnelState.UP) {
|
if (vpnService.getState() == TunnelState.UP) {
|
||||||
serviceManager.stopVpnServiceForeground(
|
serviceManager.stopVpnServiceForeground(
|
||||||
|
|
|
@ -7,16 +7,16 @@ enum class TunnelState {
|
||||||
DOWN,
|
DOWN,
|
||||||
TOGGLE;
|
TOGGLE;
|
||||||
|
|
||||||
fun toWgState() : Tunnel.State {
|
fun toWgState(): Tunnel.State {
|
||||||
return when(this) {
|
return when (this) {
|
||||||
UP -> Tunnel.State.UP
|
UP -> Tunnel.State.UP
|
||||||
DOWN -> Tunnel.State.DOWN
|
DOWN -> Tunnel.State.DOWN
|
||||||
TOGGLE -> Tunnel.State.TOGGLE
|
TOGGLE -> Tunnel.State.TOGGLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toAmState() : org.amnezia.awg.backend.Tunnel.State {
|
fun toAmState(): org.amnezia.awg.backend.Tunnel.State {
|
||||||
return when(this) {
|
return when (this) {
|
||||||
UP -> org.amnezia.awg.backend.Tunnel.State.UP
|
UP -> org.amnezia.awg.backend.Tunnel.State.UP
|
||||||
DOWN -> org.amnezia.awg.backend.Tunnel.State.DOWN
|
DOWN -> org.amnezia.awg.backend.Tunnel.State.DOWN
|
||||||
TOGGLE -> org.amnezia.awg.backend.Tunnel.State.TOGGLE
|
TOGGLE -> org.amnezia.awg.backend.Tunnel.State.TOGGLE
|
||||||
|
@ -24,15 +24,16 @@ enum class TunnelState {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun from(state: Tunnel.State) : TunnelState {
|
fun from(state: Tunnel.State): TunnelState {
|
||||||
return when(state) {
|
return when (state) {
|
||||||
Tunnel.State.DOWN -> DOWN
|
Tunnel.State.DOWN -> DOWN
|
||||||
Tunnel.State.TOGGLE -> TOGGLE
|
Tunnel.State.TOGGLE -> TOGGLE
|
||||||
Tunnel.State.UP -> UP
|
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.DOWN -> DOWN
|
||||||
org.amnezia.awg.backend.Tunnel.State.TOGGLE -> TOGGLE
|
org.amnezia.awg.backend.Tunnel.State.TOGGLE -> TOGGLE
|
||||||
org.amnezia.awg.backend.Tunnel.State.UP -> UP
|
org.amnezia.awg.backend.Tunnel.State.UP -> UP
|
||||||
|
|
|
@ -6,6 +6,8 @@ import com.wireguard.android.backend.Tunnel.State
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
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.Kernel
|
||||||
import com.zaneschepke.wireguardautotunnel.module.Userspace
|
import com.zaneschepke.wireguardautotunnel.module.Userspace
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics
|
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.service.tunnel.statistics.WireGuardStatistics
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.amnezia.awg.backend.Tunnel
|
import org.amnezia.awg.backend.Tunnel
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -28,15 +31,16 @@ import javax.inject.Inject
|
||||||
class WireGuardTunnel
|
class WireGuardTunnel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val userspaceAmneziaBackend : org.amnezia.awg.backend.Backend,
|
private val userspaceAmneziaBackend: org.amnezia.awg.backend.Backend,
|
||||||
@Userspace private val userspaceBackend: Backend,
|
@Userspace private val userspaceBackend: Backend,
|
||||||
@Kernel private val kernelBackend: Backend,
|
@Kernel private val kernelBackend: Backend,
|
||||||
private val appDataRepository: AppDataRepository,
|
private val appDataRepository: AppDataRepository,
|
||||||
|
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||||
|
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
||||||
) : VpnService {
|
) : VpnService {
|
||||||
private val _vpnState = MutableStateFlow(VpnState())
|
private val _vpnState = MutableStateFlow(VpnState())
|
||||||
override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow()
|
override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow()
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO)
|
|
||||||
|
|
||||||
private var statsJob: Job? = null
|
private var statsJob: Job? = null
|
||||||
|
|
||||||
|
@ -47,20 +51,20 @@ constructor(
|
||||||
private var backendIsAmneziaUserspace = false
|
private var backendIsAmneziaUserspace = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
scope.launch {
|
applicationScope.launch(ioDispatcher) {
|
||||||
appDataRepository.settings.getSettingsFlow().collect {
|
appDataRepository.settings.getSettingsFlow().collect {
|
||||||
if (it.isKernelEnabled && (backendIsWgUserspace || backendIsAmneziaUserspace)) {
|
if (it.isKernelEnabled && (backendIsWgUserspace || backendIsAmneziaUserspace)) {
|
||||||
Timber.d("Setting kernel backend")
|
Timber.i("Setting kernel backend")
|
||||||
backend = kernelBackend
|
backend = kernelBackend
|
||||||
backendIsWgUserspace = false
|
backendIsWgUserspace = false
|
||||||
backendIsAmneziaUserspace = false
|
backendIsAmneziaUserspace = false
|
||||||
} else if (!it.isKernelEnabled && !it.isAmneziaEnabled && !backendIsWgUserspace) {
|
} else if (!it.isKernelEnabled && !it.isAmneziaEnabled && !backendIsWgUserspace) {
|
||||||
Timber.d("Setting WireGuard userspace backend")
|
Timber.i("Setting WireGuard userspace backend")
|
||||||
backend = userspaceBackend
|
backend = userspaceBackend
|
||||||
backendIsWgUserspace = true
|
backendIsWgUserspace = true
|
||||||
backendIsAmneziaUserspace = false
|
backendIsAmneziaUserspace = false
|
||||||
} else if (it.isAmneziaEnabled && !backendIsAmneziaUserspace) {
|
} else if (it.isAmneziaEnabled && !backendIsAmneziaUserspace) {
|
||||||
Timber.d("Setting Amnezia userspace backend")
|
Timber.i("Setting Amnezia userspace backend")
|
||||||
backendIsAmneziaUserspace = true
|
backendIsAmneziaUserspace = true
|
||||||
backendIsWgUserspace = false
|
backendIsWgUserspace = false
|
||||||
}
|
}
|
||||||
|
@ -68,11 +72,11 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setState(tunnelConfig: TunnelConfig?, tunnelState: TunnelState) : TunnelState {
|
private fun setState(tunnelConfig: TunnelConfig?, tunnelState: TunnelState): TunnelState {
|
||||||
return if(backendIsAmneziaUserspace) {
|
return if (backendIsAmneziaUserspace) {
|
||||||
Timber.i("Using Amnezia backend")
|
Timber.i("Using Amnezia backend")
|
||||||
val config = tunnelConfig?.let {
|
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.")
|
Timber.w("Using backwards compatible wg config, amnezia specific config not found.")
|
||||||
TunnelConfig.configFromAmQuick(it.wgQuick)
|
TunnelConfig.configFromAmQuick(it.wgQuick)
|
||||||
}
|
}
|
||||||
|
@ -92,20 +96,22 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig?): TunnelState {
|
override suspend fun startTunnel(tunnelConfig: TunnelConfig?): TunnelState {
|
||||||
return try {
|
return withContext(ioDispatcher) {
|
||||||
//TODO we need better error handling here
|
try {
|
||||||
val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel()
|
//TODO we need better error handling here
|
||||||
if (config != null) {
|
val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel()
|
||||||
emitTunnelConfig(config)
|
if (config != null) {
|
||||||
setState(config, TunnelState.UP)
|
emitTunnelConfig(config)
|
||||||
} else throw Exception("No tunnels")
|
setState(config, TunnelState.UP)
|
||||||
} catch (e: BackendException) {
|
} else throw Exception("No tunnels")
|
||||||
Timber.e("Failed to start tunnel with error: ${e.message}")
|
} catch (e: BackendException) {
|
||||||
TunnelState.from(State.DOWN)
|
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.tryEmit(
|
||||||
_vpnState.value.copy(
|
_vpnState.value.copy(
|
||||||
status = state,
|
status = state,
|
||||||
|
@ -134,21 +140,23 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun stopTunnel() {
|
override suspend fun stopTunnel() {
|
||||||
try {
|
withContext(ioDispatcher) {
|
||||||
if (getState() == TunnelState.UP) {
|
try {
|
||||||
val state = setState(null, TunnelState.DOWN)
|
if (getState() == TunnelState.UP) {
|
||||||
resetVpnState()
|
val state = setState(null, TunnelState.DOWN)
|
||||||
emitTunnelState(state)
|
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 {
|
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))
|
else TunnelState.from(backend.getState(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,31 +170,31 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleStateChange(state: TunnelState) {
|
private fun handleStateChange(state: TunnelState) {
|
||||||
val tunnel = this
|
|
||||||
emitTunnelState(state)
|
emitTunnelState(state)
|
||||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
||||||
if (state == TunnelState.UP) {
|
if (state == TunnelState.UP) {
|
||||||
statsJob =
|
statsJob = startTunnelStatisticsJob()
|
||||||
scope.launch {
|
|
||||||
while (true) {
|
|
||||||
if(backendIsAmneziaUserspace) {
|
|
||||||
emitBackendStatistics(AmneziaStatistics(userspaceAmneziaBackend.getStatistics(tunnel)))
|
|
||||||
} else {
|
|
||||||
emitBackendStatistics(WireGuardStatistics(backend.getStatistics(tunnel)))
|
|
||||||
}
|
|
||||||
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (state == TunnelState.DOWN) {
|
if (state == TunnelState.DOWN) {
|
||||||
try {
|
try {
|
||||||
statsJob?.cancel()
|
statsJob?.cancel()
|
||||||
} catch (e : CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
Timber.i("Stats job cancelled")
|
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) {
|
override fun onStateChange(state: State) {
|
||||||
handleStateChange(TunnelState.from(state))
|
handleStateChange(TunnelState.from(state))
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ class AmneziaStatistics(private val statistics: Statistics) : TunnelStatistics()
|
||||||
PeerStats(
|
PeerStats(
|
||||||
rxBytes = stats.rxBytes,
|
rxBytes = stats.rxBytes,
|
||||||
txBytes = stats.txBytes,
|
txBytes = stats.txBytes,
|
||||||
latestHandshakeEpochMillis = stats.latestHandshakeEpochMillis
|
latestHandshakeEpochMillis = stats.latestHandshakeEpochMillis,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,17 +2,17 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
|
||||||
|
|
||||||
import org.amnezia.awg.crypto.Key
|
import org.amnezia.awg.crypto.Key
|
||||||
|
|
||||||
abstract class TunnelStatistics {
|
abstract class TunnelStatistics {
|
||||||
@JvmRecord
|
@JvmRecord
|
||||||
data class PeerStats(val rxBytes: Long, val txBytes: Long, val latestHandshakeEpochMillis: Long)
|
data class PeerStats(val rxBytes: Long, val txBytes: Long, val latestHandshakeEpochMillis: Long)
|
||||||
|
|
||||||
abstract fun peerStats(peer: Key): PeerStats?
|
abstract fun peerStats(peer: Key): PeerStats?
|
||||||
|
|
||||||
abstract fun isTunnelStale() : Boolean
|
abstract fun isTunnelStale(): Boolean
|
||||||
|
|
||||||
abstract fun getPeers(): Array<Key>
|
abstract fun getPeers(): Array<Key>
|
||||||
|
|
||||||
abstract fun rx() : Long
|
abstract fun rx(): Long
|
||||||
|
|
||||||
abstract fun tx() : Long
|
abstract fun tx(): Long
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
|
||||||
import com.wireguard.android.backend.Statistics
|
import com.wireguard.android.backend.Statistics
|
||||||
import org.amnezia.awg.crypto.Key
|
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? {
|
override fun peerStats(peer: Key): PeerStats? {
|
||||||
val key = com.wireguard.crypto.Key.fromBase64(peer.toBase64())
|
val key = com.wireguard.crypto.Key.fromBase64(peer.toBase64())
|
||||||
val peerStats = statistics.peer(key)
|
val peerStats = statistics.peer(key)
|
||||||
|
@ -11,7 +11,7 @@ class WireGuardStatistics(private val statistics: Statistics) : TunnelStatistics
|
||||||
PeerStats(
|
PeerStats(
|
||||||
txBytes = peerStats.txBytes,
|
txBytes = peerStats.txBytes,
|
||||||
rxBytes = peerStats.rxBytes,
|
rxBytes = peerStats.rxBytes,
|
||||||
latestHandshakeEpochMillis = peerStats.latestHandshakeEpochMillis
|
latestHandshakeEpochMillis = peerStats.latestHandshakeEpochMillis,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,25 +4,16 @@ import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.wireguard.android.backend.GoBackend
|
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.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.time.Instant
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
|
@ -50,7 +41,7 @@ constructor() : ViewModel() {
|
||||||
private fun requestPermissions() {
|
private fun requestPermissions() {
|
||||||
_appUiState.update {
|
_appUiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
requestPermissions = true
|
requestPermissions = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,12 +49,12 @@ constructor() : ViewModel() {
|
||||||
fun permissionsRequested() {
|
fun permissionsRequested() {
|
||||||
_appUiState.update {
|
_appUiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
requestPermissions = false
|
requestPermissions = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openWebPage(url: String, context : Context) {
|
fun openWebPage(url: String, context: Context) {
|
||||||
try {
|
try {
|
||||||
val webpage: Uri = Uri.parse(url)
|
val webpage: Uri = Uri.parse(url)
|
||||||
val intent = Intent(Intent.ACTION_VIEW, webpage).apply {
|
val intent = Intent(Intent.ACTION_VIEW, webpage).apply {
|
||||||
|
@ -79,7 +70,7 @@ constructor() : ViewModel() {
|
||||||
fun onVpnPermissionAccepted() {
|
fun onVpnPermissionAccepted() {
|
||||||
_appUiState.update {
|
_appUiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
vpnPermissionAccepted = true
|
vpnPermissionAccepted = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -122,33 +113,6 @@ constructor() : ViewModel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val logs = mutableStateListOf<LogMessage>()
|
|
||||||
|
|
||||||
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) {
|
fun setNotificationPermissionAccepted(accepted: Boolean) {
|
||||||
_appUiState.update {
|
_appUiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
|
|
|
@ -143,7 +143,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
if (!appUiState.vpnPermissionAccepted) {
|
if (!appUiState.vpnPermissionAccepted) {
|
||||||
return@LaunchedEffect appViewModel.vpnIntent?.let {
|
return@LaunchedEffect appViewModel.vpnIntent?.let {
|
||||||
vpnActivityResultState.launch(
|
vpnActivityResultState.launch(
|
||||||
it
|
it,
|
||||||
)
|
)
|
||||||
}!!
|
}!!
|
||||||
}
|
}
|
||||||
|
@ -155,7 +155,6 @@ class MainActivity : AppCompatActivity() {
|
||||||
appViewModel.setNotificationPermissionAccepted(
|
appViewModel.setNotificationPermissionAccepted(
|
||||||
notificationPermissionState?.status?.isGranted ?: true,
|
notificationPermissionState?.status?.isGranted ?: true,
|
||||||
)
|
)
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) appViewModel.readLogCatOutput()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(appUiState.snackbarMessageConsumed) {
|
LaunchedEffect(appUiState.snackbarMessageConsumed) {
|
||||||
|
@ -237,30 +236,33 @@ class MainActivity : AppCompatActivity() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(Screen.Support.Logs.route) {
|
composable(Screen.Support.Logs.route) {
|
||||||
LogsScreen(appViewModel)
|
LogsScreen()
|
||||||
}
|
}
|
||||||
//TODO fix navigation for amnezia
|
composable(
|
||||||
composable("${Screen.Config.route}/{id}?configType={configType}", arguments =
|
"${Screen.Config.route}/{id}?configType={configType}",
|
||||||
listOf(
|
arguments =
|
||||||
navArgument("id") {
|
listOf(
|
||||||
type = NavType.StringType
|
navArgument("id") {
|
||||||
defaultValue = "0"
|
type = NavType.StringType
|
||||||
},
|
defaultValue = "0"
|
||||||
navArgument("configType") {
|
},
|
||||||
type = NavType.StringType
|
navArgument("configType") {
|
||||||
defaultValue = ConfigType.WIREGUARD.name
|
type = NavType.StringType
|
||||||
}
|
defaultValue = ConfigType.WIREGUARD.name
|
||||||
)
|
},
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
val id = it.arguments?.getString("id")
|
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()) {
|
if (!id.isNullOrBlank()) {
|
||||||
ConfigScreen(
|
ConfigScreen(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
tunnelId = id,
|
tunnelId = id,
|
||||||
appViewModel = appViewModel,
|
appViewModel = appViewModel,
|
||||||
focusRequester = focusRequester,
|
focusRequester = focusRequester,
|
||||||
configType = configType
|
configType = configType,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
||||||
|
@ -52,9 +53,10 @@ fun RowListItem(
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth(13 / 20f),
|
||||||
) {
|
) {
|
||||||
icon()
|
icon()
|
||||||
Text(text)
|
Text(text, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||||
}
|
}
|
||||||
rowButton()
|
rowButton()
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,12 +28,15 @@ fun ConfigurationToggle(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
Text(label, textAlign = TextAlign.Start, modifier = Modifier
|
Text(
|
||||||
.weight(
|
label, textAlign = TextAlign.Start,
|
||||||
weight = 1.0f,
|
modifier = Modifier
|
||||||
fill = false,
|
.weight(
|
||||||
),
|
weight = 1.0f,
|
||||||
softWrap = true)
|
fill = false,
|
||||||
|
),
|
||||||
|
softWrap = true,
|
||||||
|
)
|
||||||
Switch(
|
Switch(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
|
|
|
@ -483,7 +483,7 @@ fun ConfigScreen(
|
||||||
modifier = Modifier.width(IntrinsicSize.Min),
|
modifier = Modifier.width(IntrinsicSize.Min),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if(configType == ConfigType.AMNEZIA) {
|
if (configType == ConfigType.AMNEZIA) {
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = uiState.interfaceProxy.junkPacketCount,
|
value = uiState.interfaceProxy.junkPacketCount,
|
||||||
onValueChange = { value -> viewModel.onJunkPacketCountChanged(value) },
|
onValueChange = { value -> viewModel.onJunkPacketCountChanged(value) },
|
||||||
|
@ -496,7 +496,11 @@ fun ConfigScreen(
|
||||||
)
|
)
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = uiState.interfaceProxy.junkPacketMinSize,
|
value = uiState.interfaceProxy.junkPacketMinSize,
|
||||||
onValueChange = { value -> viewModel.onJunkPacketMinSizeChanged(value) },
|
onValueChange = { value ->
|
||||||
|
viewModel.onJunkPacketMinSizeChanged(
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
},
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.junk_packet_minimum_size),
|
label = stringResource(R.string.junk_packet_minimum_size),
|
||||||
hint = stringResource(R.string.junk_packet_minimum_size).lowercase(),
|
hint = stringResource(R.string.junk_packet_minimum_size).lowercase(),
|
||||||
|
@ -506,7 +510,11 @@ fun ConfigScreen(
|
||||||
)
|
)
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = uiState.interfaceProxy.junkPacketMaxSize,
|
value = uiState.interfaceProxy.junkPacketMaxSize,
|
||||||
onValueChange = { value -> viewModel.onJunkPacketMaxSizeChanged(value) },
|
onValueChange = { value ->
|
||||||
|
viewModel.onJunkPacketMaxSizeChanged(
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
},
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.junk_packet_maximum_size),
|
label = stringResource(R.string.junk_packet_maximum_size),
|
||||||
hint = stringResource(R.string.junk_packet_maximum_size).lowercase(),
|
hint = stringResource(R.string.junk_packet_maximum_size).lowercase(),
|
||||||
|
@ -516,7 +524,11 @@ fun ConfigScreen(
|
||||||
)
|
)
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = uiState.interfaceProxy.initPacketJunkSize,
|
value = uiState.interfaceProxy.initPacketJunkSize,
|
||||||
onValueChange = { value -> viewModel.onInitPacketJunkSizeChanged(value) },
|
onValueChange = { value ->
|
||||||
|
viewModel.onInitPacketJunkSizeChanged(
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
},
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.init_packet_junk_size),
|
label = stringResource(R.string.init_packet_junk_size),
|
||||||
hint = stringResource(R.string.init_packet_junk_size).lowercase(),
|
hint = stringResource(R.string.init_packet_junk_size).lowercase(),
|
||||||
|
@ -546,7 +558,11 @@ fun ConfigScreen(
|
||||||
)
|
)
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = uiState.interfaceProxy.responsePacketMagicHeader,
|
value = uiState.interfaceProxy.responsePacketMagicHeader,
|
||||||
onValueChange = { value -> viewModel.onResponsePacketMagicHeader(value) },
|
onValueChange = { value ->
|
||||||
|
viewModel.onResponsePacketMagicHeader(
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
},
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.response_packet_magic_header),
|
label = stringResource(R.string.response_packet_magic_header),
|
||||||
hint = stringResource(R.string.response_packet_magic_header).lowercase(),
|
hint = stringResource(R.string.response_packet_magic_header).lowercase(),
|
||||||
|
@ -556,7 +572,11 @@ fun ConfigScreen(
|
||||||
)
|
)
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = uiState.interfaceProxy.underloadPacketMagicHeader,
|
value = uiState.interfaceProxy.underloadPacketMagicHeader,
|
||||||
onValueChange = { value -> viewModel.onUnderloadPacketMagicHeader(value) },
|
onValueChange = { value ->
|
||||||
|
viewModel.onUnderloadPacketMagicHeader(
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
},
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.underload_packet_magic_header),
|
label = stringResource(R.string.underload_packet_magic_header),
|
||||||
hint = stringResource(R.string.underload_packet_magic_header).lowercase(),
|
hint = stringResource(R.string.underload_packet_magic_header).lowercase(),
|
||||||
|
@ -566,7 +586,11 @@ fun ConfigScreen(
|
||||||
)
|
)
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = uiState.interfaceProxy.transportPacketMagicHeader,
|
value = uiState.interfaceProxy.transportPacketMagicHeader,
|
||||||
onValueChange = { value -> viewModel.onTransportPacketMagicHeader(value) },
|
onValueChange = { value ->
|
||||||
|
viewModel.onTransportPacketMagicHeader(
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
},
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.transport_packet_magic_header),
|
label = stringResource(R.string.transport_packet_magic_header),
|
||||||
hint = stringResource(R.string.transport_packet_magic_header).lowercase(),
|
hint = stringResource(R.string.transport_packet_magic_header).lowercase(),
|
||||||
|
|
|
@ -19,7 +19,7 @@ data class ConfigUiState(
|
||||||
val isAmneziaEnabled: Boolean = false
|
val isAmneziaEnabled: Boolean = false
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun from(config : Config) : ConfigUiState {
|
fun from(config: Config): ConfigUiState {
|
||||||
val proxyPeers = config.peers.map { PeerProxy.from(it) }
|
val proxyPeers = config.peers.map { PeerProxy.from(it) }
|
||||||
val proxyInterface = InterfaceProxy.from(config.`interface`)
|
val proxyInterface = InterfaceProxy.from(config.`interface`)
|
||||||
var include = true
|
var include = true
|
||||||
|
@ -43,7 +43,8 @@ data class ConfigUiState(
|
||||||
isAllApplicationsEnabled,
|
isAllApplicationsEnabled,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
fun from(config: org.amnezia.awg.config.Config) : ConfigUiState {
|
|
||||||
|
fun from(config: org.amnezia.awg.config.Config): ConfigUiState {
|
||||||
//TODO update with new values
|
//TODO update with new values
|
||||||
val proxyPeers = config.peers.map { PeerProxy.from(it) }
|
val proxyPeers = config.peers.map { PeerProxy.from(it) }
|
||||||
val proxyInterface = InterfaceProxy.from(config.`interface`)
|
val proxyInterface = InterfaceProxy.from(config.`interface`)
|
||||||
|
|
|
@ -16,6 +16,7 @@ import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
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.config.model.PeerProxy
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
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.removeAt
|
||||||
import com.zaneschepke.wireguardautotunnel.util.update
|
import com.zaneschepke.wireguardautotunnel.util.update
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
@ -38,7 +39,8 @@ class ConfigViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val settingsRepository: SettingsRepository,
|
private val settingsRepository: SettingsRepository,
|
||||||
private val appDataRepository: AppDataRepository
|
private val appDataRepository: AppDataRepository,
|
||||||
|
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val packageManager = WireGuardAutoTunnel.instance.packageManager
|
private val packageManager = WireGuardAutoTunnel.instance.packageManager
|
||||||
|
@ -47,7 +49,7 @@ constructor(
|
||||||
val uiState = _uiState.asStateFlow()
|
val uiState = _uiState.asStateFlow()
|
||||||
|
|
||||||
fun init(tunnelId: String) =
|
fun init(tunnelId: String) =
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(ioDispatcher) {
|
||||||
val packages = getQueriedPackages("")
|
val packages = getQueriedPackages("")
|
||||||
val state =
|
val state =
|
||||||
if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
||||||
|
@ -56,15 +58,16 @@ constructor(
|
||||||
.firstOrNull { it.id.toString() == tunnelId }
|
.firstOrNull { it.id.toString() == tunnelId }
|
||||||
val isAmneziaEnabled = settingsRepository.getSettings().isAmneziaEnabled
|
val isAmneziaEnabled = settingsRepository.getSettings().isAmneziaEnabled
|
||||||
if (tunnelConfig != null) {
|
if (tunnelConfig != null) {
|
||||||
(if(isAmneziaEnabled) {
|
(if (isAmneziaEnabled) {
|
||||||
val amConfig = if(tunnelConfig.amQuick == "") tunnelConfig.wgQuick else tunnelConfig.amQuick
|
val amConfig =
|
||||||
|
if (tunnelConfig.amQuick == "") tunnelConfig.wgQuick else tunnelConfig.amQuick
|
||||||
ConfigUiState.from(TunnelConfig.configFromAmQuick(amConfig))
|
ConfigUiState.from(TunnelConfig.configFromAmQuick(amConfig))
|
||||||
} else ConfigUiState.from(TunnelConfig.configFromWgQuick(tunnelConfig.wgQuick))).copy(
|
} else ConfigUiState.from(TunnelConfig.configFromWgQuick(tunnelConfig.wgQuick))).copy(
|
||||||
packages = packages,
|
packages = packages,
|
||||||
loading = false,
|
loading = false,
|
||||||
tunnel = tunnelConfig,
|
tunnel = tunnelConfig,
|
||||||
tunnelName = tunnelConfig.name,
|
tunnelName = tunnelConfig.name,
|
||||||
isAmneziaEnabled = isAmneziaEnabled
|
isAmneziaEnabled = isAmneziaEnabled,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
ConfigUiState(loading = false, packages = packages)
|
ConfigUiState(loading = false, packages = packages)
|
||||||
|
@ -206,64 +209,82 @@ constructor(
|
||||||
if (isAllApplicationsEnabled()) emptyCheckedPackagesList()
|
if (isAllApplicationsEnabled()) emptyCheckedPackagesList()
|
||||||
if (_uiState.value.include) builder.includeApplications(_uiState.value.checkedPackageNames)
|
if (_uiState.value.include) builder.includeApplications(_uiState.value.checkedPackageNames)
|
||||||
if (!_uiState.value.include) builder.excludeApplications(_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())
|
builder.setJunkPacketCount(_uiState.value.interfaceProxy.junkPacketCount.trim().toInt())
|
||||||
}
|
}
|
||||||
if(_uiState.value.interfaceProxy.junkPacketMinSize.isNotEmpty()) {
|
if (_uiState.value.interfaceProxy.junkPacketMinSize.isNotEmpty()) {
|
||||||
builder.setJunkPacketMinSize(_uiState.value.interfaceProxy.junkPacketMinSize.trim().toInt())
|
builder.setJunkPacketMinSize(
|
||||||
|
_uiState.value.interfaceProxy.junkPacketMinSize.trim().toInt(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if(_uiState.value.interfaceProxy.junkPacketMaxSize.isNotEmpty()) {
|
if (_uiState.value.interfaceProxy.junkPacketMaxSize.isNotEmpty()) {
|
||||||
builder.setJunkPacketMaxSize(_uiState.value.interfaceProxy.junkPacketMaxSize.trim().toInt())
|
builder.setJunkPacketMaxSize(
|
||||||
|
_uiState.value.interfaceProxy.junkPacketMaxSize.trim().toInt(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if(_uiState.value.interfaceProxy.initPacketJunkSize.isNotEmpty()) {
|
if (_uiState.value.interfaceProxy.initPacketJunkSize.isNotEmpty()) {
|
||||||
builder.setInitPacketJunkSize(_uiState.value.interfaceProxy.initPacketJunkSize.trim().toInt())
|
builder.setInitPacketJunkSize(
|
||||||
|
_uiState.value.interfaceProxy.initPacketJunkSize.trim().toInt(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if(_uiState.value.interfaceProxy.responsePacketJunkSize.isNotEmpty()) {
|
if (_uiState.value.interfaceProxy.responsePacketJunkSize.isNotEmpty()) {
|
||||||
builder.setResponsePacketJunkSize(_uiState.value.interfaceProxy.responsePacketJunkSize.trim().toInt())
|
builder.setResponsePacketJunkSize(
|
||||||
|
_uiState.value.interfaceProxy.responsePacketJunkSize.trim().toInt(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if(_uiState.value.interfaceProxy.initPacketMagicHeader.isNotEmpty()) {
|
if (_uiState.value.interfaceProxy.initPacketMagicHeader.isNotEmpty()) {
|
||||||
builder.setInitPacketMagicHeader(_uiState.value.interfaceProxy.initPacketMagicHeader.trim().toLong())
|
builder.setInitPacketMagicHeader(
|
||||||
|
_uiState.value.interfaceProxy.initPacketMagicHeader.trim().toLong(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if(_uiState.value.interfaceProxy.responsePacketMagicHeader.isNotEmpty()) {
|
if (_uiState.value.interfaceProxy.responsePacketMagicHeader.isNotEmpty()) {
|
||||||
builder.setResponsePacketMagicHeader(_uiState.value.interfaceProxy.responsePacketMagicHeader.trim().toLong())
|
builder.setResponsePacketMagicHeader(
|
||||||
|
_uiState.value.interfaceProxy.responsePacketMagicHeader.trim().toLong(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if(_uiState.value.interfaceProxy.transportPacketMagicHeader.isNotEmpty()) {
|
if (_uiState.value.interfaceProxy.transportPacketMagicHeader.isNotEmpty()) {
|
||||||
builder.setTransportPacketMagicHeader(_uiState.value.interfaceProxy.transportPacketMagicHeader.trim().toLong())
|
builder.setTransportPacketMagicHeader(
|
||||||
|
_uiState.value.interfaceProxy.transportPacketMagicHeader.trim().toLong(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if(_uiState.value.interfaceProxy.underloadPacketMagicHeader.isNotEmpty()) {
|
if (_uiState.value.interfaceProxy.underloadPacketMagicHeader.isNotEmpty()) {
|
||||||
builder.setUnderloadPacketMagicHeader(_uiState.value.interfaceProxy.underloadPacketMagicHeader.trim().toLong())
|
builder.setUnderloadPacketMagicHeader(
|
||||||
|
_uiState.value.interfaceProxy.underloadPacketMagicHeader.trim().toLong(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildConfig() : Config {
|
private fun buildConfig(): Config {
|
||||||
val peerList = buildPeerListFromProxyPeers()
|
val peerList = buildPeerListFromProxyPeers()
|
||||||
val wgInterface = buildInterfaceListFromProxyInterface()
|
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 peerList = buildAmPeerListFromProxyPeers()
|
||||||
val amInterface = buildAmInterfaceListFromProxyInterface()
|
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<Unit> {
|
fun onSaveAllChanges(configType: ConfigType): Result<Unit> {
|
||||||
return try {
|
return try {
|
||||||
val wgQuick = buildConfig().toWgQuickString()
|
val wgQuick = buildConfig().toWgQuickString()
|
||||||
val amQuick = if(configType == ConfigType.AMNEZIA) {
|
val amQuick = if (configType == ConfigType.AMNEZIA) {
|
||||||
buildAmConfig().toAwgQuickString()
|
buildAmConfig().toAwgQuickString()
|
||||||
} else TunnelConfig.AM_QUICK_DEFAULT
|
} else TunnelConfig.AM_QUICK_DEFAULT
|
||||||
val tunnelConfig = when (uiState.value.tunnel) {
|
val tunnelConfig = when (uiState.value.tunnel) {
|
||||||
null -> TunnelConfig(
|
null -> TunnelConfig(
|
||||||
name = _uiState.value.tunnelName,
|
name = _uiState.value.tunnelName,
|
||||||
wgQuick = wgQuick,
|
wgQuick = wgQuick,
|
||||||
amQuick = amQuick
|
amQuick = amQuick,
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> uiState.value.tunnel!!.copy(
|
else -> uiState.value.tunnel!!.copy(
|
||||||
name = _uiState.value.tunnelName,
|
name = _uiState.value.tunnelName,
|
||||||
wgQuick = wgQuick,
|
wgQuick = wgQuick,
|
||||||
amQuick = amQuick
|
amQuick = amQuick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
updateTunnelConfig(tunnelConfig)
|
updateTunnelConfig(tunnelConfig)
|
||||||
|
@ -430,14 +451,15 @@ constructor(
|
||||||
fun onJunkPacketCountChanged(value: String) {
|
fun onJunkPacketCountChanged(value: String) {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketCount = value)
|
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketCount = value),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onJunkPacketMinSizeChanged(value: String) {
|
fun onJunkPacketMinSizeChanged(value: String) {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
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) {
|
fun onJunkPacketMaxSizeChanged(value: String) {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
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) {
|
fun onInitPacketJunkSizeChanged(value: String) {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
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) {
|
fun onResponsePacketJunkSize(value: String) {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
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) {
|
fun onInitPacketMagicHeader(value: String) {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
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) {
|
fun onResponsePacketMagicHeader(value: String) {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
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) {
|
fun onTransportPacketMagicHeader(value: String) {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
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) {
|
fun onUnderloadPacketMagicHeader(value: String) {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
interfaceProxy = _uiState.value.interfaceProxy.copy(underloadPacketMagicHeader = value)
|
interfaceProxy = _uiState.value.interfaceProxy.copy(underloadPacketMagicHeader = value),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,8 @@ data class InterfaceProxy(
|
||||||
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "",
|
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(
|
return InterfaceProxy(
|
||||||
publicKey = i.keyPair.publicKey.toBase64().trim(),
|
publicKey = i.keyPair.publicKey.toBase64().trim(),
|
||||||
privateKey = i.keyPair.privateKey.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 "",
|
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "",
|
||||||
junkPacketCount = if(i.junkPacketCount.isPresent) i.junkPacketCount.get().toString() else "",
|
junkPacketCount = if (i.junkPacketCount.isPresent) i.junkPacketCount.get()
|
||||||
junkPacketMinSize = if(i.junkPacketMinSize.isPresent) i.junkPacketMinSize.get().toString() else "",
|
.toString() else "",
|
||||||
junkPacketMaxSize = if(i.junkPacketMaxSize.isPresent) i.junkPacketMaxSize.get().toString() else "",
|
junkPacketMinSize = if (i.junkPacketMinSize.isPresent) i.junkPacketMinSize.get()
|
||||||
initPacketJunkSize = if(i.initPacketJunkSize.isPresent) i.initPacketJunkSize.get().toString() else "",
|
.toString() else "",
|
||||||
responsePacketJunkSize = if(i.responsePacketJunkSize.isPresent) i.responsePacketJunkSize.get().toString() else "",
|
junkPacketMaxSize = if (i.junkPacketMaxSize.isPresent) i.junkPacketMaxSize.get()
|
||||||
initPacketMagicHeader = if(i.initPacketMagicHeader.isPresent) i.initPacketMagicHeader.get().toString() else "",
|
.toString() else "",
|
||||||
responsePacketMagicHeader = if(i.responsePacketMagicHeader.isPresent) i.responsePacketMagicHeader.get().toString() else "",
|
initPacketJunkSize = if (i.initPacketJunkSize.isPresent) i.initPacketJunkSize.get()
|
||||||
transportPacketMagicHeader = if(i.transportPacketMagicHeader.isPresent) i.transportPacketMagicHeader.get().toString() else "",
|
.toString() else "",
|
||||||
underloadPacketMagicHeader = if(i.underloadPacketMagicHeader.isPresent) i.underloadPacketMagicHeader.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 "",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
return PeerProxy(
|
||||||
publicKey = peer.publicKey.toBase64(),
|
publicKey = peer.publicKey.toBase64(),
|
||||||
preSharedKey =
|
preSharedKey =
|
||||||
|
|
|
@ -14,6 +14,7 @@ import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.focusGroup
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
import androidx.compose.foundation.gestures.ScrollableDefaults
|
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
@ -69,7 +70,6 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
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.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
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.getMessage
|
||||||
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
|
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
||||||
import com.zaneschepke.wireguardautotunnel.util.truncateWithEllipsis
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@ -128,7 +126,7 @@ fun MainScreen(
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val isVisible = rememberSaveable { mutableStateOf(true) }
|
val isVisible = rememberSaveable { mutableStateOf(true) }
|
||||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
val sheetState = rememberModalBottomSheetState()
|
val sheetState = rememberModalBottomSheetState()
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
|
@ -212,8 +210,8 @@ fun MainScreen(
|
||||||
onResult = {
|
onResult = {
|
||||||
if (it.contents != null) {
|
if (it.contents != null) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
viewModel.onTunnelQrResult(it.contents, configType).onFailure {
|
viewModel.onTunnelQrResult(it.contents, configType).onFailure { error ->
|
||||||
appViewModel.showSnackbarMessage(it.getMessage(context))
|
appViewModel.showSnackbarMessage(error.getMessage(context))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -246,7 +244,9 @@ fun MainScreen(
|
||||||
|
|
||||||
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
|
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
|
||||||
if (appViewModel.isRequiredPermissionGranted()) {
|
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(
|
Scaffold(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.pointerInput(Unit) {
|
Modifier.pointerInput(Unit) {
|
||||||
if(uiState.tunnels.isNotEmpty()) {
|
if (uiState.tunnels.isNotEmpty()) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onTap = {
|
onTap = {
|
||||||
selectedTunnel = null
|
selectedTunnel = null
|
||||||
|
@ -285,31 +285,25 @@ fun MainScreen(
|
||||||
visible = isVisible.value,
|
visible = isVisible.value,
|
||||||
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
||||||
exit = slideOutVertically(targetOffsetY = { it * 2 }),
|
exit = slideOutVertically(targetOffsetY = { it * 2 }),
|
||||||
|
modifier = Modifier
|
||||||
|
.focusRequester(focusRequester)
|
||||||
|
.focusGroup(),
|
||||||
) {
|
) {
|
||||||
val secondaryColor = MaterialTheme.colorScheme.secondary
|
val secondaryColor = MaterialTheme.colorScheme.secondary
|
||||||
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
val tvFobColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||||
var fobColor by remember { mutableStateOf(secondaryColor) }
|
val fobColor =
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) tvFobColor else secondaryColor
|
||||||
|
val fobIconColor =
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) Color.White else MaterialTheme.colorScheme.background
|
||||||
MultiFloatingActionButton(
|
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(
|
fabIcon = FabIcon(
|
||||||
iconRes = R.drawable.add,
|
iconRes = R.drawable.add,
|
||||||
iconResAfterRotate = R.drawable.close,
|
iconResAfterRotate = R.drawable.close,
|
||||||
iconRotate = 180f
|
iconRotate = 180f,
|
||||||
),
|
),
|
||||||
fabOption = FabOption(
|
fabOption = FabOption(
|
||||||
iconTint = MaterialTheme.colorScheme.background,
|
iconTint = fobIconColor,
|
||||||
backgroundTint = MaterialTheme.colorScheme.primary,
|
backgroundTint = fobColor,
|
||||||
),
|
),
|
||||||
itemsMultiFab = listOf(
|
itemsMultiFab = listOf(
|
||||||
MultiFabItem(
|
MultiFabItem(
|
||||||
|
@ -318,24 +312,39 @@ fun MainScreen(
|
||||||
stringResource(id = R.string.amnezia),
|
stringResource(id = R.string.amnezia),
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.padding(end = 10.dp)
|
modifier = Modifier.padding(end = 10.dp),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp),
|
||||||
icon = R.drawable.add,
|
icon = R.drawable.add,
|
||||||
value = ConfigType.AMNEZIA.name,
|
value = ConfigType.AMNEZIA.name,
|
||||||
|
miniFabOption = FabOption(
|
||||||
|
backgroundTint = fobColor,
|
||||||
|
fobIconColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
MultiFabItem(
|
MultiFabItem(
|
||||||
label = {
|
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,
|
icon = R.drawable.add,
|
||||||
value = ConfigType.WIREGUARD.name
|
value = ConfigType.WIREGUARD.name,
|
||||||
|
miniFabOption = FabOption(
|
||||||
|
backgroundTint = fobColor,
|
||||||
|
fobIconColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onFabItemClicked = {
|
onFabItemClicked = {
|
||||||
showBottomSheet = true
|
showBottomSheet = true
|
||||||
configType = ConfigType.valueOf(it.value)
|
configType = ConfigType.valueOf(it.value)
|
||||||
},
|
},
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -343,7 +352,10 @@ fun MainScreen(
|
||||||
) {
|
) {
|
||||||
if (showBottomSheet) {
|
if (showBottomSheet) {
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = { showBottomSheet = false },
|
onDismissRequest = {
|
||||||
|
showBottomSheet = false
|
||||||
|
|
||||||
|
},
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
) {
|
) {
|
||||||
// Sheet content
|
// Sheet content
|
||||||
|
@ -432,34 +444,48 @@ fun MainScreen(
|
||||||
flingBehavior = ScrollableDefaults.flingBehavior(),
|
flingBehavior = ScrollableDefaults.flingBehavior(),
|
||||||
) {
|
) {
|
||||||
item {
|
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(
|
AnimatedVisibility(
|
||||||
uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) {
|
uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn(),
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
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)
|
val gettingStarted = buildAnnotatedString {
|
||||||
ClickableText(
|
append(stringResource(id = R.string.see_the))
|
||||||
modifier = Modifier.padding(vertical = 10.dp, horizontal = 24.dp),
|
append(" ")
|
||||||
text = gettingStarted,
|
pushStringAnnotation(
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center),
|
tag = "gettingStarted",
|
||||||
) {
|
annotation = stringResource(id = R.string.getting_started_url),
|
||||||
gettingStarted.getStringAnnotations(tag = "gettingStarted", it, it).firstOrNull()?.let { annotation ->
|
)
|
||||||
appViewModel.openWebPage(annotation.item, context)
|
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),
|
.size(if (icon == circleIcon) 15.dp else 20.dp),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
text = tunnel.name.truncateWithEllipsis(Constants.ALLOWED_DISPLAY_NAME_LENGTH),
|
text = tunnel.name,
|
||||||
onHold = {
|
onHold = {
|
||||||
if (
|
if (
|
||||||
(uiState.vpnState.status == TunnelState.UP) &&
|
(uiState.vpnState.status == TunnelState.UP) &&
|
||||||
|
|
|
@ -11,6 +11,7 @@ import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
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.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
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.WgTunnelExceptions
|
||||||
import com.zaneschepke.wireguardautotunnel.util.toWgQuickString
|
import com.zaneschepke.wireguardautotunnel.util.toWgQuickString
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
@ -35,7 +36,8 @@ class MainViewModel
|
||||||
constructor(
|
constructor(
|
||||||
private val appDataRepository: AppDataRepository,
|
private val appDataRepository: AppDataRepository,
|
||||||
private val serviceManager: ServiceManager,
|
private val serviceManager: ServiceManager,
|
||||||
val vpnService: VpnService
|
val vpnService: VpnService,
|
||||||
|
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val uiState =
|
val uiState =
|
||||||
|
@ -52,13 +54,12 @@ constructor(
|
||||||
MainUiState(),
|
MainUiState(),
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun stopWatcherService(context: Context) =
|
private fun stopWatcherService(context: Context) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
serviceManager.stopWatcherService(context)
|
||||||
serviceManager.stopWatcherService(context)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun onDelete(tunnel: TunnelConfig, context: Context) {
|
fun onDelete(tunnel: TunnelConfig, context: Context) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch {
|
||||||
val settings = appDataRepository.settings.getSettings()
|
val settings = appDataRepository.settings.getSettings()
|
||||||
val isPrimary = tunnel.isPrimaryTunnel
|
val isPrimary = tunnel.isPrimaryTunnel
|
||||||
if (appDataRepository.tunnels.count() == 1 || isPrimary) {
|
if (appDataRepository.tunnels.count() == 1 || isPrimary) {
|
||||||
|
@ -80,7 +81,7 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTunnelStart(tunnelConfig: TunnelConfig, context: Context) =
|
fun onTunnelStart(tunnelConfig: TunnelConfig, context: Context) =
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch {
|
||||||
Timber.d("On start called!")
|
Timber.d("On start called!")
|
||||||
serviceManager.startVpnService(
|
serviceManager.startVpnService(
|
||||||
context,
|
context,
|
||||||
|
@ -91,41 +92,42 @@ constructor(
|
||||||
|
|
||||||
|
|
||||||
fun onTunnelStop(context: Context) =
|
fun onTunnelStop(context: Context) =
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch {
|
||||||
Timber.i("Stopping active tunnel")
|
Timber.i("Stopping active tunnel")
|
||||||
serviceManager.stopVpnService(context, isManualStop = true)
|
serviceManager.stopVpnService(context, isManualStop = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validateConfigString(config: String, configType: ConfigType) {
|
private fun validateConfigString(config: String, configType: ConfigType) {
|
||||||
when(configType) {
|
when (configType) {
|
||||||
ConfigType.AMNEZIA -> TunnelConfig.configFromAmQuick(config)
|
ConfigType.AMNEZIA -> TunnelConfig.configFromAmQuick(config)
|
||||||
ConfigType.WIREGUARD -> TunnelConfig.configFromWgQuick(config)
|
ConfigType.WIREGUARD -> TunnelConfig.configFromWgQuick(config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateQrCodeDefaultName(config : String, configType: ConfigType) : String {
|
private fun generateQrCodeDefaultName(config: String, configType: ConfigType): String {
|
||||||
return try {
|
return try {
|
||||||
when(configType) {
|
when (configType) {
|
||||||
ConfigType.AMNEZIA -> {
|
ConfigType.AMNEZIA -> {
|
||||||
TunnelConfig.configFromAmQuick(config).peers[0].endpoint.get().host
|
TunnelConfig.configFromAmQuick(config).peers[0].endpoint.get().host
|
||||||
}
|
}
|
||||||
|
|
||||||
ConfigType.WIREGUARD -> {
|
ConfigType.WIREGUARD -> {
|
||||||
TunnelConfig.configFromWgQuick(config).peers[0].endpoint.get().host
|
TunnelConfig.configFromWgQuick(config).peers[0].endpoint.get().host
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e : Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
NumberUtils.generateRandomTunnelName()
|
NumberUtils.generateRandomTunnelName()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateQrCodeTunnelName(config : String, configType: ConfigType) : String {
|
private fun generateQrCodeTunnelName(config: String, configType: ConfigType): String {
|
||||||
var defaultName = generateQrCodeDefaultName(config, configType)
|
var defaultName = generateQrCodeDefaultName(config, configType)
|
||||||
val lines = config.lines().toMutableList()
|
val lines = config.lines().toMutableList()
|
||||||
val linesIterator = lines.iterator()
|
val linesIterator = lines.iterator()
|
||||||
while(linesIterator.hasNext()) {
|
while (linesIterator.hasNext()) {
|
||||||
val next = linesIterator.next()
|
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()
|
defaultName = next.substringAfter(Constants.QR_CODE_NAME_PROPERTY).trim()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -134,121 +136,177 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onTunnelQrResult(result: String, configType: ConfigType): Result<Unit> {
|
suspend fun onTunnelQrResult(result: String, configType: ConfigType): Result<Unit> {
|
||||||
return try {
|
return withContext(ioDispatcher) {
|
||||||
validateConfigString(result, configType)
|
try {
|
||||||
val tunnelName = makeTunnelNameUnique(generateQrCodeTunnelName(result, configType))
|
validateConfigString(result, configType)
|
||||||
val tunnelConfig = when(configType) {
|
val tunnelName = makeTunnelNameUnique(generateQrCodeTunnelName(result, configType))
|
||||||
ConfigType.AMNEZIA ->{
|
val tunnelConfig = when (configType) {
|
||||||
TunnelConfig(name = tunnelName, amQuick = result,
|
ConfigType.AMNEZIA -> {
|
||||||
wgQuick = TunnelConfig.configFromAmQuick(result).toWgQuickString())
|
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 {
|
private suspend fun makeTunnelNameUnique(name: String): String {
|
||||||
val tunnels = appDataRepository.tunnels.getAll()
|
return withContext(ioDispatcher) {
|
||||||
var tunnelName = name
|
val tunnels = appDataRepository.tunnels.getAll()
|
||||||
var num = 1
|
var tunnelName = name
|
||||||
while (tunnels.any { it.name == tunnelName }) {
|
var num = 1
|
||||||
tunnelName = name + "(${num})"
|
while (tunnels.any { it.name == tunnelName }) {
|
||||||
num++
|
tunnelName = name + "(${num})"
|
||||||
|
num++
|
||||||
|
}
|
||||||
|
tunnelName
|
||||||
}
|
}
|
||||||
return tunnelName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String, type: ConfigType) {
|
private fun saveTunnelConfigFromStream(
|
||||||
var amQuick : String? = null
|
stream: InputStream,
|
||||||
|
fileName: String,
|
||||||
|
type: ConfigType
|
||||||
|
) {
|
||||||
|
var amQuick: String? = null
|
||||||
val wgQuick = stream.use {
|
val wgQuick = stream.use {
|
||||||
when(type) {
|
when (type) {
|
||||||
ConfigType.AMNEZIA -> {
|
ConfigType.AMNEZIA -> {
|
||||||
val config = org.amnezia.awg.config.Config.parse(it)
|
val config = org.amnezia.awg.config.Config.parse(it)
|
||||||
amQuick = config.toAwgQuickString()
|
amQuick = config.toAwgQuickString()
|
||||||
config.toWgQuickString()
|
config.toWgQuickString()
|
||||||
}
|
}
|
||||||
|
|
||||||
ConfigType.WIREGUARD -> {
|
ConfigType.WIREGUARD -> {
|
||||||
Config.parse(it).toWgQuickString()
|
Config.parse(it).toWgQuickString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val tunnelName = makeTunnelNameUnique(getNameFromFileName(fileName))
|
viewModelScope.launch {
|
||||||
addTunnel(TunnelConfig(name = tunnelName, wgQuick = wgQuick, amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT))
|
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? {
|
private fun getInputStreamFromUri(uri: Uri, context: Context): InputStream? {
|
||||||
return context.applicationContext.contentResolver.openInputStream(uri)
|
return context.applicationContext.contentResolver.openInputStream(uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onTunnelFileSelected(uri: Uri, configType: ConfigType, context: Context): Result<Unit> {
|
suspend fun onTunnelFileSelected(
|
||||||
return try {
|
uri: Uri,
|
||||||
if (isValidUriContentScheme(uri)) {
|
configType: ConfigType,
|
||||||
val fileName = getFileName(context, uri)
|
context: Context
|
||||||
return when (getFileExtensionFromFileName(fileName)) {
|
): Result<Unit> {
|
||||||
Constants.CONF_FILE_EXTENSION ->
|
return withContext(ioDispatcher) {
|
||||||
saveTunnelFromConfUri(fileName, uri, configType, context)
|
try {
|
||||||
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri, configType, context)
|
if (isValidUriContentScheme(uri)) {
|
||||||
else -> Result.failure(WgTunnelExceptions.InvalidFileExtension())
|
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 {
|
} catch (e: Exception) {
|
||||||
Result.failure(WgTunnelExceptions.InvalidFileExtension())
|
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<Unit> {
|
private suspend fun saveTunnelsFromZipUri(
|
||||||
return ZipInputStream(getInputStreamFromUri(uri, context)).use { zip ->
|
uri: Uri,
|
||||||
generateSequence { zip.nextEntry }
|
configType: ConfigType,
|
||||||
.filterNot {
|
context: Context
|
||||||
it.isDirectory ||
|
): Result<Unit> {
|
||||||
getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
|
return withContext(ioDispatcher) {
|
||||||
}
|
ZipInputStream(getInputStreamFromUri(uri, context)).use { zip ->
|
||||||
.forEach {
|
generateSequence { zip.nextEntry }
|
||||||
val name = getNameFromFileName(it.name)
|
.filterNot {
|
||||||
withContext(viewModelScope.coroutineContext + Dispatchers.IO) {
|
it.isDirectory ||
|
||||||
try {
|
getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
|
||||||
var amQuick : String? = null
|
}
|
||||||
val wgQuick =
|
.forEach {
|
||||||
when(configType) {
|
val name = getNameFromFileName(it.name)
|
||||||
ConfigType.AMNEZIA -> {
|
withContext(viewModelScope.coroutineContext) {
|
||||||
val config = org.amnezia.awg.config.Config.parse(zip)
|
try {
|
||||||
amQuick = config.toAwgQuickString()
|
var amQuick: String? = null
|
||||||
config.toWgQuickString()
|
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 -> {
|
addTunnel(
|
||||||
Config.parse(zip).toWgQuickString()
|
TunnelConfig(
|
||||||
}
|
name = makeTunnelNameUnique(name),
|
||||||
}
|
wgQuick = wgQuick,
|
||||||
addTunnel(TunnelConfig(name = makeTunnelNameUnique(name), wgQuick = wgQuick, amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT))
|
amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT,
|
||||||
Result.success(Unit)
|
),
|
||||||
} catch (e : Exception) {
|
)
|
||||||
Result.failure(WgTunnelExceptions.FileReadFailed())
|
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<Unit> {
|
||||||
|
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<Unit> {
|
private fun addTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
|
||||||
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) {
|
|
||||||
val firstTunnel = appDataRepository.tunnels.count() == 0
|
val firstTunnel = appDataRepository.tunnels.count() == 0
|
||||||
saveTunnel(tunnelConfig)
|
saveTunnel(tunnelConfig)
|
||||||
if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
||||||
|
@ -266,7 +324,7 @@ constructor(
|
||||||
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
|
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
|
private fun saveTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
|
||||||
appDataRepository.tunnels.save(tunnelConfig)
|
appDataRepository.tunnels.save(tunnelConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -317,7 +375,7 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveSettings(settings: Settings) =
|
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 {
|
fun onCopyTunnel(tunnel: TunnelConfig?) = viewModelScope.launch {
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.options
|
package com.zaneschepke.wireguardautotunnel.ui.screens.options
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
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.clickable
|
||||||
|
import androidx.compose.foundation.focusGroup
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
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.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
@ -39,7 +44,6 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
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.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
@ -114,59 +118,75 @@ fun OptionsScreen(
|
||||||
Scaffold(
|
Scaffold(
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
val secondaryColor = MaterialTheme.colorScheme.secondary
|
val secondaryColor = MaterialTheme.colorScheme.secondary
|
||||||
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
val tvFobColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||||
var fobColor by remember { mutableStateOf(secondaryColor) }
|
val fobColor =
|
||||||
MultiFloatingActionButton(
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) tvFobColor else secondaryColor
|
||||||
modifier =
|
val fobIconColor =
|
||||||
(if (
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) Color.White else MaterialTheme.colorScheme.background
|
||||||
WireGuardAutoTunnel.isRunningOnAndroidTv()
|
AnimatedVisibility(
|
||||||
)
|
visible = true,
|
||||||
Modifier.focusRequester(focusRequester)
|
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
||||||
else Modifier)
|
exit = slideOutVertically(targetOffsetY = { it * 2 }),
|
||||||
.onFocusChanged {
|
modifier = Modifier
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
.focusRequester(focusRequester)
|
||||||
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
.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(
|
shape = RoundedCornerShape(16.dp),
|
||||||
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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
|
|
@ -9,7 +9,6 @@ import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
|
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
@ -45,12 +44,12 @@ constructor(
|
||||||
fun init(tunnelId: String) {
|
fun init(tunnelId: String) {
|
||||||
_optionState.update {
|
_optionState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
id = tunnelId
|
id = tunnelId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDeleteRunSSID(ssid: String) = viewModelScope.launch(Dispatchers.IO) {
|
fun onDeleteRunSSID(ssid: String) = viewModelScope.launch {
|
||||||
uiState.value.tunnel?.let {
|
uiState.value.tunnel?.let {
|
||||||
appDataRepository.tunnels.save(
|
appDataRepository.tunnels.save(
|
||||||
tunnelConfig = it.copy(
|
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 {
|
tunnelConfig?.let {
|
||||||
appDataRepository.tunnels.save(it)
|
appDataRepository.tunnels.save(it)
|
||||||
}
|
}
|
||||||
|
@ -81,7 +80,7 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleIsMobileDataTunnel() = viewModelScope.launch(Dispatchers.IO) {
|
fun onToggleIsMobileDataTunnel() = viewModelScope.launch {
|
||||||
uiState.value.tunnel?.let {
|
uiState.value.tunnel?.let {
|
||||||
if (it.isMobileDataTunnel) {
|
if (it.isMobileDataTunnel) {
|
||||||
appDataRepository.tunnels.updateMobileDataTunnel(null)
|
appDataRepository.tunnels.updateMobileDataTunnel(null)
|
||||||
|
@ -89,7 +88,7 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTogglePrimaryTunnel() = viewModelScope.launch(Dispatchers.IO) {
|
fun onTogglePrimaryTunnel() = viewModelScope.launch {
|
||||||
if (uiState.value.tunnel != null) {
|
if (uiState.value.tunnel != null) {
|
||||||
appDataRepository.tunnels.updatePrimaryTunnel(
|
appDataRepository.tunnels.updatePrimaryTunnel(
|
||||||
when (uiState.value.isDefaultTunnel) {
|
when (uiState.value.isDefaultTunnel) {
|
||||||
|
|
|
@ -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.config.ConfigurationToggle
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.getMessage
|
import com.zaneschepke.wireguardautotunnel.util.getMessage
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||||
|
@ -100,9 +98,9 @@ fun SettingsScreen(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
focusRequester: FocusRequester,
|
focusRequester: FocusRequester,
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
val pinExists = remember { mutableStateOf(PinManager.pinExists()) }
|
val pinExists = remember { mutableStateOf(PinManager.pinExists()) }
|
||||||
|
@ -137,18 +135,22 @@ fun SettingsScreen(
|
||||||
}
|
}
|
||||||
file
|
file
|
||||||
}
|
}
|
||||||
val amFiles = uiState.tunnels.mapNotNull { config -> if(config.amQuick != TunnelConfig.AM_QUICK_DEFAULT) {
|
val amFiles = uiState.tunnels.mapNotNull { config ->
|
||||||
val file = File(context.cacheDir, "${config.name}-am.conf")
|
if (config.amQuick != TunnelConfig.AM_QUICK_DEFAULT) {
|
||||||
file.outputStream().use {
|
val file = File(context.cacheDir, "${config.name}-am.conf")
|
||||||
it.write(config.amQuick.toByteArray())
|
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) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
|
@ -190,11 +192,9 @@ fun SettingsScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openSettings() {
|
fun openSettings() {
|
||||||
scope.launch {
|
val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||||
val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
|
intentSettings.data = Uri.fromParts("package", context.packageName, null)
|
||||||
intentSettings.data = Uri.fromParts("package", context.packageName, null)
|
context.startActivity(intentSettings)
|
||||||
context.startActivity(intentSettings)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkFineLocationGranted() {
|
fun checkFineLocationGranted() {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
|
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
@ -19,6 +20,7 @@ import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
|
@ -28,6 +30,7 @@ constructor(
|
||||||
private val appDataRepository: AppDataRepository,
|
private val appDataRepository: AppDataRepository,
|
||||||
private val serviceManager: ServiceManager,
|
private val serviceManager: ServiceManager,
|
||||||
private val rootShell: RootShell,
|
private val rootShell: RootShell,
|
||||||
|
private val fileUtils: FileUtils,
|
||||||
vpnService: VpnService
|
vpnService: VpnService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
@ -90,6 +93,10 @@ constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun onExportTunnels(files: List<File>): Result<Unit> {
|
||||||
|
return fileUtils.saveFilesToZip(files)
|
||||||
|
}
|
||||||
|
|
||||||
fun onToggleAutoTunnel(context: Context) =
|
fun onToggleAutoTunnel(context: Context) =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
|
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
|
||||||
|
@ -160,7 +167,7 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleAmnezia() = viewModelScope.launch {
|
fun onToggleAmnezia() = viewModelScope.launch {
|
||||||
if(uiState.value.settings.isKernelEnabled) {
|
if (uiState.value.settings.isKernelEnabled) {
|
||||||
saveKernelMode(false)
|
saveKernelMode(false)
|
||||||
}
|
}
|
||||||
saveAmneziaMode(!uiState.value.settings.isAmneziaEnabled)
|
saveAmneziaMode(!uiState.value.settings.isAmneziaEnabled)
|
||||||
|
@ -169,8 +176,8 @@ constructor(
|
||||||
private fun saveAmneziaMode(on: Boolean) {
|
private fun saveAmneziaMode(on: Boolean) {
|
||||||
saveSettings(
|
saveSettings(
|
||||||
uiState.value.settings.copy(
|
uiState.value.settings.copy(
|
||||||
isAmneziaEnabled = on
|
isAmneziaEnabled = on,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -107,7 +107,12 @@ fun SupportScreen(
|
||||||
modifier = Modifier.padding(bottom = 20.dp),
|
modifier = Modifier.padding(bottom = 20.dp),
|
||||||
)
|
)
|
||||||
TextButton(
|
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
|
modifier = Modifier
|
||||||
.padding(vertical = 5.dp)
|
.padding(vertical = 5.dp)
|
||||||
.focusRequester(focusRequester),
|
.focusRequester(focusRequester),
|
||||||
|
@ -129,7 +134,7 @@ fun SupportScreen(
|
||||||
weight = 1.0f,
|
weight = 1.0f,
|
||||||
fill = false,
|
fill = false,
|
||||||
),
|
),
|
||||||
softWrap = true
|
softWrap = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Icon(
|
Icon(
|
||||||
|
@ -143,7 +148,12 @@ fun SupportScreen(
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
)
|
)
|
||||||
TextButton(
|
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),
|
modifier = Modifier.padding(vertical = 5.dp),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
@ -175,7 +185,12 @@ fun SupportScreen(
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
)
|
)
|
||||||
TextButton(
|
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),
|
modifier = Modifier.padding(vertical = 5.dp),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
@ -269,7 +284,10 @@ fun SupportScreen(
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.clickable {
|
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(
|
Row(
|
||||||
|
@ -285,7 +303,7 @@ fun SupportScreen(
|
||||||
val mode = buildAnnotatedString {
|
val mode = buildAnnotatedString {
|
||||||
append(stringResource(R.string.mode))
|
append(stringResource(R.string.mode))
|
||||||
append(": ")
|
append(": ")
|
||||||
when(uiState.settings.isKernelEnabled){
|
when (uiState.settings.isKernelEnabled) {
|
||||||
true -> append(stringResource(id = R.string.kernel))
|
true -> append(stringResource(id = R.string.kernel))
|
||||||
false -> append(stringResource(id = R.string.userspace))
|
false -> append(stringResource(id = R.string.userspace))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support.logs
|
package com.zaneschepke.wireguardautotunnel.ui.screens.support.logs
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
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.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
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.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
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.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
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 com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
@Composable
|
@Composable
|
||||||
fun LogsScreen(appViewModel: AppViewModel) {
|
fun LogsScreen(viewModel: LogsViewModel = hiltViewModel()) {
|
||||||
|
|
||||||
val logs = remember {
|
val logs = viewModel.logs
|
||||||
appViewModel.logs
|
|
||||||
}
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
@ -60,7 +61,15 @@ fun LogsScreen(appViewModel: AppViewModel) {
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
onClick = {
|
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),
|
shape = RoundedCornerShape(16.dp),
|
||||||
containerColor = MaterialTheme.colorScheme.primary,
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
|
@ -82,7 +91,11 @@ fun LogsScreen(appViewModel: AppViewModel) {
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = 24.dp),
|
.padding(horizontal = 24.dp),
|
||||||
) {
|
) {
|
||||||
items(logs) {
|
itemsIndexed(
|
||||||
|
logs,
|
||||||
|
key = { index, _ -> index },
|
||||||
|
contentType = { _: Int, _: LogMessage -> null },
|
||||||
|
) { _, it ->
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start),
|
horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start),
|
||||||
verticalAlignment = Alignment.Top,
|
verticalAlignment = Alignment.Top,
|
||||||
|
|
|
@ -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<LogMessage>()
|
||||||
|
|
||||||
|
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<Unit> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,10 +16,11 @@ object Constants {
|
||||||
const val URI_CONTENT_SCHEME = "content"
|
const val URI_CONTENT_SCHEME = "content"
|
||||||
const val ALLOWED_FILE_TYPES = "*/*"
|
const val ALLOWED_FILE_TYPES = "*/*"
|
||||||
const val TEXT_MIME_TYPE = "text/plain"
|
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 GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs"
|
||||||
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
||||||
const val ALWAYS_ON_VPN_ACTION = "android.net.VpnService"
|
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 SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024
|
||||||
|
|
||||||
const val SUBSCRIPTION_TIMEOUT = 5_000L
|
const val SUBSCRIPTION_TIMEOUT = 5_000L
|
||||||
|
|
|
@ -9,13 +9,27 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.GlobalScope
|
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.launch
|
||||||
|
import kotlinx.coroutines.selects.whileSelect
|
||||||
import org.amnezia.awg.config.Config
|
import org.amnezia.awg.config.Config
|
||||||
|
import timber.log.Timber
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
|
import java.time.Duration
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
fun BroadcastReceiver.goAsync(
|
fun BroadcastReceiver.goAsync(
|
||||||
context: CoroutineContext = EmptyCoroutineContext,
|
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 {
|
fun BigDecimal.toThreeDecimalPlaceString(): String {
|
||||||
val df = DecimalFormat("#.###")
|
val df = DecimalFormat("#.###")
|
||||||
return df.format(this)
|
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 amQuick = toAwgQuickString()
|
||||||
val lines = amQuick.lines().toMutableList()
|
val lines = amQuick.lines().toMutableList()
|
||||||
val linesIterator = lines.iterator()
|
val linesIterator = lines.iterator()
|
||||||
while(linesIterator.hasNext()) {
|
while (linesIterator.hasNext()) {
|
||||||
val next = linesIterator.next()
|
val next = linesIterator.next()
|
||||||
Constants.amneziaProperties.forEach {
|
Constants.amneziaProperties.forEach {
|
||||||
if(next.startsWith(it, ignoreCase = true)) {
|
if (next.startsWith(it, ignoreCase = true)) {
|
||||||
linesIterator.remove()
|
linesIterator.remove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,9 +96,73 @@ fun Config.toWgQuickString() : String {
|
||||||
return lines.joinToString(System.lineSeparator())
|
return lines.joinToString(System.lineSeparator())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Throwable.getMessage(context: Context) : String {
|
fun Throwable.getMessage(context: Context): String {
|
||||||
return when(this) {
|
return when (this) {
|
||||||
is WgTunnelExceptions -> this.getMessage(context)
|
is WgTunnelExceptions -> this.getMessage(context)
|
||||||
else -> this.message ?: StringValue.StringResource(R.string.unknown_error).asString(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 <T> ReceiveChannel<T>.chunked(scope: CoroutineScope, size: Int, time: Duration) =
|
||||||
|
scope.produce<List<T>> {
|
||||||
|
while (true) { // this loop goes over each chunk
|
||||||
|
val chunk = ConcurrentLinkedQueue<T>() // 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 <T> Flow<T>.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 <T> CoroutineScope.asChannel(flow: Flow<T>): ReceiveChannel<T> = produce {
|
||||||
|
flow.collect { value ->
|
||||||
|
channel.send(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,20 +6,102 @@ import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.provider.MediaStore.MediaColumns
|
import android.provider.MediaStore.MediaColumns
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
object FileUtils {
|
class FileUtils(
|
||||||
private const val ZIP_FILE_MIME_TYPE = "application/zip"
|
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<Unit> {
|
||||||
|
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<File>): Result<Unit> {
|
||||||
|
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
|
//TODO issue with android 9
|
||||||
private fun createDownloadsFileOutputStream(
|
private fun createDownloadsFileOutputStream(
|
||||||
context: Context,
|
|
||||||
fileName: String,
|
fileName: String,
|
||||||
mimeType: String = Constants.ALLOWED_FILE_TYPES
|
mimeType: String = Constants.ALLOWED_FILE_TYPES
|
||||||
): OutputStream? {
|
): OutputStream? {
|
||||||
|
@ -45,53 +127,4 @@ object FileUtils {
|
||||||
}
|
}
|
||||||
return null
|
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<File>) : Result<Unit> {
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,121 +4,127 @@ import android.content.Context
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
|
||||||
sealed class WgTunnelExceptions : Exception() {
|
sealed class WgTunnelExceptions : Exception() {
|
||||||
abstract fun getMessage(context: Context) : String
|
abstract fun getMessage(context: Context): String
|
||||||
data class General(private val userMessage : StringValue) : WgTunnelExceptions() {
|
data class General(private val userMessage: StringValue) : WgTunnelExceptions() {
|
||||||
override fun getMessage(context: Context) : String {
|
override fun getMessage(context: Context): String {
|
||||||
return userMessage.asString(context)
|
return userMessage.asString(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class SsidConflict(private val userMessage : StringValue = StringValue.StringResource(R.string.error_ssid_exists)) : WgTunnelExceptions() {
|
data class SsidConflict(private val userMessage: StringValue = StringValue.StringResource(R.string.error_ssid_exists)) :
|
||||||
override fun getMessage(context: Context) : String {
|
WgTunnelExceptions() {
|
||||||
|
override fun getMessage(context: Context): String {
|
||||||
return userMessage.asString(context)
|
return userMessage.asString(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ConfigExportFailed(private val userMessage : StringValue = StringValue.StringResource(R.string.export_configs_failed)) : WgTunnelExceptions() {
|
data class ConfigExportFailed(
|
||||||
override fun getMessage(context: Context) : String {
|
private val userMessage: StringValue = StringValue.StringResource(
|
||||||
|
R.string.export_configs_failed,
|
||||||
|
)
|
||||||
|
) : WgTunnelExceptions() {
|
||||||
|
override fun getMessage(context: Context): String {
|
||||||
return userMessage.asString(context)
|
return userMessage.asString(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ConfigParseError(private val appendMessage : StringValue = StringValue.Empty) : WgTunnelExceptions() {
|
data class ConfigParseError(private val appendMessage: StringValue = StringValue.Empty) :
|
||||||
override fun getMessage(context: Context) : String {
|
WgTunnelExceptions() {
|
||||||
|
override fun getMessage(context: Context): String {
|
||||||
return StringValue.StringResource(R.string.config_parse_error).asString(context) + (
|
return StringValue.StringResource(R.string.config_parse_error).asString(context) + (
|
||||||
if (appendMessage != StringValue.Empty) ": ${appendMessage.asString(context)}" else "")
|
if (appendMessage != StringValue.Empty) ": ${appendMessage.asString(context)}" else "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class RootDenied(private val userMessage : StringValue = StringValue.StringResource(R.string.error_root_denied)) : WgTunnelExceptions() {
|
data class RootDenied(private val userMessage: StringValue = StringValue.StringResource(R.string.error_root_denied)) :
|
||||||
override fun getMessage(context: Context) : String {
|
WgTunnelExceptions() {
|
||||||
|
override fun getMessage(context: Context): String {
|
||||||
return userMessage.asString(context)
|
return userMessage.asString(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class InvalidQrCode(private val userMessage : StringValue = StringValue.StringResource(R.string.error_invalid_code)) : WgTunnelExceptions() {
|
data class InvalidQrCode(private val userMessage: StringValue = StringValue.StringResource(R.string.error_invalid_code)) :
|
||||||
override fun getMessage(context: Context) : String {
|
WgTunnelExceptions() {
|
||||||
|
override fun getMessage(context: Context): String {
|
||||||
return userMessage.asString(context)
|
return userMessage.asString(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class InvalidFileExtension(private val userMessage : StringValue = StringValue.StringResource(R.string.error_file_extension)) : WgTunnelExceptions() {
|
data class InvalidFileExtension(
|
||||||
override fun getMessage(context: Context) : String {
|
private val userMessage: StringValue = StringValue.StringResource(
|
||||||
|
R.string.error_file_extension,
|
||||||
|
)
|
||||||
|
) : WgTunnelExceptions() {
|
||||||
|
override fun getMessage(context: Context): String {
|
||||||
return userMessage.asString(context)
|
return userMessage.asString(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class FileReadFailed(private val userMessage : StringValue = StringValue.StringResource(R.string.error_file_format)) : WgTunnelExceptions() {
|
data class FileReadFailed(private val userMessage: StringValue = StringValue.StringResource(R.string.error_file_format)) :
|
||||||
override fun getMessage(context: Context) : String {
|
WgTunnelExceptions() {
|
||||||
|
override fun getMessage(context: Context): String {
|
||||||
return userMessage.asString(context)
|
return userMessage.asString(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class AuthenticationFailed(private val userMessage : StringValue = StringValue.StringResource(R.string.error_authentication_failed)) : WgTunnelExceptions() {
|
data class AuthenticationFailed(
|
||||||
override fun getMessage(context: Context) : String {
|
private val userMessage: StringValue = StringValue.StringResource(
|
||||||
|
R.string.error_authentication_failed,
|
||||||
|
)
|
||||||
|
) : WgTunnelExceptions() {
|
||||||
|
override fun getMessage(context: Context): String {
|
||||||
return userMessage.asString(context)
|
return userMessage.asString(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class AuthorizationFailed(private val userMessage : StringValue = StringValue.StringResource(R.string.error_authorization_failed)) : WgTunnelExceptions() {
|
data class AuthorizationFailed(
|
||||||
override fun getMessage(context: Context) : String {
|
private val userMessage: StringValue = StringValue.StringResource(
|
||||||
|
R.string.error_authorization_failed,
|
||||||
|
)
|
||||||
|
) : WgTunnelExceptions() {
|
||||||
|
override fun getMessage(context: Context): String {
|
||||||
return userMessage.asString(context)
|
return userMessage.asString(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class BackgroundLocationRequired(private val userMessage : StringValue = StringValue.StringResource(R.string.background_location_required)) : WgTunnelExceptions() {
|
data class BackgroundLocationRequired(
|
||||||
override fun getMessage(context: Context) : String {
|
private val userMessage: StringValue = StringValue.StringResource(
|
||||||
|
R.string.background_location_required,
|
||||||
|
)
|
||||||
|
) : WgTunnelExceptions() {
|
||||||
|
override fun getMessage(context: Context): String {
|
||||||
return userMessage.asString(context)
|
return userMessage.asString(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class LocationServicesRequired(private val userMessage : StringValue = StringValue.StringResource(R.string.location_services_required)) : WgTunnelExceptions() {
|
data class LocationServicesRequired(
|
||||||
override fun getMessage(context: Context) : String {
|
private val userMessage: StringValue = StringValue.StringResource(
|
||||||
|
R.string.location_services_required,
|
||||||
|
)
|
||||||
|
) : WgTunnelExceptions() {
|
||||||
|
override fun getMessage(context: Context): String {
|
||||||
return userMessage.asString(context)
|
return userMessage.asString(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class PreciseLocationRequired(private val userMessage : StringValue = StringValue.StringResource(R.string.precise_location_required)) : WgTunnelExceptions() {
|
data class PreciseLocationRequired(
|
||||||
override fun getMessage(context: Context) : String {
|
private val userMessage: StringValue = StringValue.StringResource(
|
||||||
|
R.string.precise_location_required,
|
||||||
|
)
|
||||||
|
) : WgTunnelExceptions() {
|
||||||
|
override fun getMessage(context: Context): String {
|
||||||
return userMessage.asString(context)
|
return userMessage.asString(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class FileExplorerRequired (private val userMessage : StringValue = StringValue.StringResource(R.string.error_no_file_explorer)) : WgTunnelExceptions() {
|
data class FileExplorerRequired(
|
||||||
override fun getMessage(context: Context) : String {
|
private val userMessage: StringValue = StringValue.StringResource(
|
||||||
|
R.string.error_no_file_explorer,
|
||||||
|
)
|
||||||
|
) : WgTunnelExceptions() {
|
||||||
|
override fun getMessage(context: Context): String {
|
||||||
return userMessage.asString(context)
|
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)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="960"
|
android:viewportWidth="960"
|
||||||
android:viewportHeight="960">
|
android:viewportHeight="960">
|
||||||
<path
|
<path
|
||||||
android:pathData="M440,520L200,520v-80h240v-240h80v240h240v80L520,520v240h-80v-240Z"
|
android:fillColor="#e8eaed"
|
||||||
android:fillColor="#e8eaed"/>
|
android:pathData="M440,520L200,520v-80h240v-240h80v240h240v80L520,520v240h-80v-240Z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="960"
|
android:viewportWidth="960"
|
||||||
android:viewportHeight="960">
|
android:viewportHeight="960">
|
||||||
<path
|
<path
|
||||||
android:pathData="m256,760 l-56,-56 224,-224 -224,-224 56,-56 224,224 224,-224 56,56 -224,224 224,224 -56,56 -224,-224 -224,224Z"
|
android:fillColor="#e8eaed"
|
||||||
android:fillColor="#e8eaed"/>
|
android:pathData="m256,760 l-56,-56 224,-224 -224,-224 56,-56 224,224 224,-224 56,56 -224,224 224,224 -56,56 -224,-224 -224,224Z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="960"
|
android:viewportWidth="960"
|
||||||
android:viewportHeight="960">
|
android:viewportHeight="960">
|
||||||
<path
|
<path
|
||||||
android:pathData="M200,760h57l391,-391 -57,-57 -391,391v57ZM120,840v-170l528,-527q12,-11 26.5,-17t30.5,-6q16,0 31,6t26,18l55,56q12,11 17.5,26t5.5,30q0,16 -5.5,30.5T817,313L290,840L120,840ZM760,256 L704,200 760,256ZM619,341 L591,312 648,369 619,341Z"
|
android:fillColor="#e8eaed"
|
||||||
android:fillColor="#e8eaed"/>
|
android:pathData="M200,760h57l391,-391 -57,-57 -391,391v57ZM120,840v-170l528,-527q12,-11 26.5,-17t30.5,-6q16,0 31,6t26,18l55,56q12,11 17.5,26t5.5,30q0,16 -5.5,30.5T817,313L290,840L120,840ZM760,256 L704,200 760,256ZM619,341 L591,312 648,369 619,341Z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
android:height="50dp"
|
android:height="50dp"
|
||||||
android:viewportWidth="50"
|
android:viewportWidth="50"
|
||||||
android:viewportHeight="50">
|
android:viewportHeight="50">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FF000000"
|
android:fillColor="#FF000000"
|
||||||
android:pathData="M25,2c12.703,0 23,10.297 23,23S37.703,48 25,48S2,37.703 2,25S12.297,2 25,2zM32.934,34.375c0.423,-1.298 2.405,-14.234 2.65,-16.783c0.074,-0.772 -0.17,-1.285 -0.648,-1.514c-0.578,-0.278 -1.434,-0.139 -2.427,0.219c-1.362,0.491 -18.774,7.884 -19.78,8.312c-0.954,0.405 -1.856,0.847 -1.856,1.487c0,0.45 0.267,0.703 1.003,0.966c0.766,0.273 2.695,0.858 3.834,1.172c1.097,0.303 2.346,0.04 3.046,-0.395c0.742,-0.461 9.305,-6.191 9.92,-6.693c0.614,-0.502 1.104,0.141 0.602,0.644c-0.502,0.502 -6.38,6.207 -7.155,6.997c-0.941,0.959 -0.273,1.953 0.358,2.351c0.721,0.454 5.906,3.932 6.687,4.49c0.781,0.558 1.573,0.811 2.298,0.811C32.191,36.439 32.573,35.484 32.934,34.375z"/>
|
android:pathData="M25,2c12.703,0 23,10.297 23,23S37.703,48 25,48S2,37.703 2,25S12.297,2 25,2zM32.934,34.375c0.423,-1.298 2.405,-14.234 2.65,-16.783c0.074,-0.772 -0.17,-1.285 -0.648,-1.514c-0.578,-0.278 -1.434,-0.139 -2.427,0.219c-1.362,0.491 -18.774,7.884 -19.78,8.312c-0.954,0.405 -1.856,0.847 -1.856,1.487c0,0.45 0.267,0.703 1.003,0.966c0.766,0.273 2.695,0.858 3.834,1.172c1.097,0.303 2.346,0.04 3.046,-0.395c0.742,-0.461 9.305,-6.191 9.92,-6.693c0.614,-0.502 1.104,0.141 0.602,0.644c-0.502,0.502 -6.38,6.207 -7.155,6.997c-0.941,0.959 -0.273,1.953 0.358,2.351c0.721,0.454 5.906,3.932 6.687,4.49c0.781,0.558 1.573,0.811 2.298,0.811C32.191,36.439 32.573,35.484 32.934,34.375z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
object Constants {
|
object Constants {
|
||||||
const val VERSION_NAME = "3.4.4"
|
const val VERSION_NAME = "3.4.5"
|
||||||
const val JVM_TARGET = "17"
|
const val JVM_TARGET = "17"
|
||||||
const val VERSION_CODE = 34400
|
const val VERSION_CODE = 34500
|
||||||
const val TARGET_SDK = 34
|
const val TARGET_SDK = 34
|
||||||
const val MIN_SDK = 26
|
const val MIN_SDK = 26
|
||||||
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
||||||
|
|
|
@ -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
|
|
@ -16,19 +16,18 @@ junit = "4.13.2"
|
||||||
kotlinx-serialization-json = "1.6.3"
|
kotlinx-serialization-json = "1.6.3"
|
||||||
lifecycle-runtime-compose = "2.7.0"
|
lifecycle-runtime-compose = "2.7.0"
|
||||||
material3 = "1.2.1"
|
material3 = "1.2.1"
|
||||||
multifabVersion = "1.0.9"
|
multifabVersion = "1.1.0"
|
||||||
navigationCompose = "2.7.7"
|
navigationCompose = "2.7.7"
|
||||||
pinLockCompose = "1.0.3"
|
pinLockCompose = "1.0.3"
|
||||||
roomVersion = "2.6.1"
|
roomVersion = "2.6.1"
|
||||||
timber = "5.0.1"
|
timber = "5.0.1"
|
||||||
tunnel = "1.0.20230706"
|
tunnel = "1.0.20230706"
|
||||||
androidGradlePlugin = "8.4.0"
|
androidGradlePlugin = "8.4.1"
|
||||||
kotlin = "1.9.23"
|
kotlin = "1.9.23"
|
||||||
ksp = "1.9.23-1.0.19"
|
ksp = "1.9.23-1.0.19"
|
||||||
composeBom = "2024.05.00"
|
composeBom = "2024.05.00"
|
||||||
compose = "1.6.7"
|
compose = "1.6.7"
|
||||||
zxingAndroidEmbedded = "4.3.0"
|
zxingAndroidEmbedded = "4.3.0"
|
||||||
zxingCore = "3.5.3"
|
|
||||||
|
|
||||||
#plugins
|
#plugins
|
||||||
gradlePlugins-kotlinxSerialization = "1.9.23"
|
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" }
|
tunnel = { module = "com.wireguard.android:tunnel", version.ref = "tunnel" }
|
||||||
|
|
||||||
zaneschepke-multifab = { module = "com.zaneschepke:multifab", version.ref = "multifabVersion" }
|
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" }
|
zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxingAndroidEmbedded" }
|
||||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#Wed Oct 11 22:39:21 EDT 2023
|
#Wed Oct 11 22:39:21 EDT 2023
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
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
|
distributionSha256Sum=9631d53cf3e74bfa726893aee1f8994fee4e060c401335946dba2156f440f24c
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
@ -40,4 +40,7 @@ dependencies {
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|
||||||
|
// logging
|
||||||
|
implementation(libs.timber)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<File>
|
||||||
|
val bufferedLogs: Flow<LogMessage>
|
||||||
|
}
|
||||||
|
|
|
@ -1,34 +1,271 @@
|
||||||
package com.zaneschepke.logcatter
|
package com.zaneschepke.logcatter
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.zaneschepke.logcatter.model.LogLevel
|
||||||
import com.zaneschepke.logcatter.model.LogMessage
|
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 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 findIpv4AddressRegex = """((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}""".toRegex()
|
||||||
private val findTunnelNameRegex = """(?<=tunnel ).*?(?= UP| DOWN)""".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)}){
|
fun init(
|
||||||
clear()
|
maxFileSize: Long = MAX_FILE_SIZE,
|
||||||
Runtime.getRuntime().exec("logcat -v epoch")
|
maxFolderSize: Long = MAX_FOLDER_SIZE,
|
||||||
.inputStream
|
context: Context
|
||||||
.bufferedReader()
|
): LocalLogCollector {
|
||||||
.useLines { lines ->
|
if (maxFileSize > maxFolderSize) {
|
||||||
lines.forEach { callback(LogMessage.from(obfuscator(it))) }
|
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 {
|
internal object Logcat : LocalLogCollector {
|
||||||
return findKeyRegex.replace(log, "<crypto-key>").let { first ->
|
|
||||||
findIpv6AddressRegex.replace(first, "<ipv6-address>").let { second ->
|
|
||||||
findTunnelNameRegex.replace(second, "<tunnel>")
|
|
||||||
}
|
|
||||||
}.let{ last -> findIpv4AddressRegex.replace(last,"<ipv4-address>") }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clear() {
|
private var logcatReader: LogcatReader? = null
|
||||||
Runtime.getRuntime().exec("logcat -c")
|
|
||||||
|
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<File> {
|
||||||
|
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<LogMessage>(
|
||||||
|
replay = 10_000,
|
||||||
|
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
override val bufferedLogs: Flow<LogMessage> = _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, "<crypto-key>").let { first ->
|
||||||
|
findIpv6AddressRegex.replace(first, "<ipv6-address>").let { second ->
|
||||||
|
findTunnelNameRegex.replace(second, "<tunnel>")
|
||||||
|
}
|
||||||
|
}.let { last -> findIpv4AddressRegex.replace(last, "<ipv4-address>") }
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,12 @@ package com.zaneschepke.logcatter.model
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
data class LogMessage(
|
data class LogMessage(
|
||||||
val time: Instant,
|
val time: String,
|
||||||
val pid: String,
|
val pid: String,
|
||||||
val tid: String,
|
val tid: String,
|
||||||
val level: LogLevel,
|
val level: LogLevel,
|
||||||
val tag: String,
|
val tag: String,
|
||||||
val message: String
|
val message: String,
|
||||||
) {
|
) {
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "$time $pid $tid $level $tag message= $message"
|
return "$time $pid $tid $level $tag message= $message"
|
||||||
|
@ -16,21 +16,22 @@ data class LogMessage(
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun from(logcatLine: String): LogMessage {
|
fun from(logcatLine: String): LogMessage {
|
||||||
return if (logcatLine.contains("---------")) LogMessage(
|
return if (logcatLine.contains("---------")) {
|
||||||
Instant.now(),
|
LogMessage(
|
||||||
"0",
|
Instant.now().toString(),
|
||||||
"0",
|
"0",
|
||||||
LogLevel.VERBOSE,
|
"0",
|
||||||
"System",
|
LogLevel.VERBOSE,
|
||||||
logcatLine,
|
"System",
|
||||||
)
|
logcatLine,
|
||||||
else {
|
)
|
||||||
//TODO improve this
|
} else {
|
||||||
|
// TODO improve this
|
||||||
val parts = logcatLine.trim().split(" ").filter { it.isNotEmpty() }
|
val parts = logcatLine.trim().split(" ").filter { it.isNotEmpty() }
|
||||||
val epochParts = parts[0].split(".").map { it.toLong() }
|
val epochParts = parts[0].split(".").map { it.toLong() }
|
||||||
val message = parts.subList(5, parts.size).joinToString(" ")
|
val message = parts.subList(5, parts.size).joinToString(" ")
|
||||||
LogMessage(
|
LogMessage(
|
||||||
Instant.ofEpochSecond(epochParts[0], epochParts[1]),
|
Instant.ofEpochSecond(epochParts[0], epochParts[1]).toString(),
|
||||||
parts[1],
|
parts[1],
|
||||||
parts[2],
|
parts[2],
|
||||||
LogLevel.fromSignifier(parts[3]),
|
LogLevel.fromSignifier(parts[3]),
|
||||||
|
|
Loading…
Reference in New Issue