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:
Zane Schepke 2024-05-30 23:10:28 -04:00
parent 57bb3f5e74
commit 54d9653f04
69 changed files with 1764 additions and 967 deletions

View File

@ -2,7 +2,7 @@ name: Issue Updates Workflow
on: on:
issues: issues:
types: [opened, closed, reopened] types: [ opened, closed, reopened ]
jobs: jobs:

View File

@ -2,7 +2,7 @@ name: Release Updates Workflow
on: on:
release: release:
types: [published] types: [ published ]
jobs: jobs:

View File

@ -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
``` ```

View File

@ -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)

View File

@ -22,5 +22,3 @@
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite { -keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
<fields>; <fields>;
} }

View File

@ -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()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build(),
)
} else Timber.plant(ReleaseTree())
applicationScope.launch(ioDispatcher) {
PinManager.initialize(this@WireGuardAutoTunnel)
if (!isRunningOnAndroidTv()) localLogCollector.start()
} }
override fun onLowMemory() {
super.onLowMemory()
applicationScope.cancel("onLowMemory() called by system")
applicationScope = MainScope()
} }
companion object { companion object {
var applicationScope = MainScope()
lateinit var instance: WireGuardAutoTunnel lateinit var instance: WireGuardAutoTunnel
private set private set

View File

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

View File

@ -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

View File

@ -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,14 +38,17 @@ class DataStoreManager(private val context: Context) {
) )
suspend fun init() { suspend fun init() {
withContext(ioDispatcher) {
try { try {
context.dataStore.data.first() context.dataStore.data.first()
} catch (e: IOException) { } catch (e: IOException) {
Timber.e(e) Timber.e(e)
} }
} }
}
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) { suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) {
withContext(ioDispatcher) {
try { try {
context.dataStore.edit { it[key] = value } context.dataStore.edit { it[key] = value }
} catch (e: IOException) { } catch (e: IOException) {
@ -48,18 +57,21 @@ class DataStoreManager(private val context: Context) {
Timber.e(e) Timber.e(e)
} }
} }
}
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) {
try {
context.dataStore.data.map { it[key] }.first() context.dataStore.data.map { it[key] }.first()
} catch (e: IOException) { } catch (e: IOException) {
Timber.e(e) Timber.e(e)
null null
} }
} }
}
fun <T> getFromStoreBlocking(key: Preferences.Key<T>) = runBlocking { fun <T> getFromStoreBlocking(key: Preferences.Key<T>) = runBlocking {

View File

@ -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 = ""
} }
} }

View File

@ -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

View File

@ -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)
}
}

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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()
}
}

View File

@ -1,7 +0,0 @@
package com.zaneschepke.wireguardautotunnel.module
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Kernel

View File

@ -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

View File

@ -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)
} }
} }

View File

@ -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)
}
}

View File

@ -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")

View File

@ -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,6 +55,7 @@ class ServiceManager(private val appDataRepository: AppDataRepository) {
} }
suspend fun stopVpnServiceForeground(context: Context, isManualStop: Boolean = false) { suspend fun stopVpnServiceForeground(context: Context, isManualStop: Boolean = false) {
withContext(ioDispatcher) {
if (isManualStop) onManualStop() if (isManualStop) onManualStop()
Timber.i("Stopping vpn service") Timber.i("Stopping vpn service")
actionOnService( actionOnService(
@ -54,8 +64,10 @@ class ServiceManager(private val appDataRepository: AppDataRepository) {
WireGuardTunnelService::class.java, WireGuardTunnelService::class.java,
) )
} }
}
suspend fun stopVpnService(context: Context, isManualStop: Boolean = false) { suspend fun stopVpnService(context: Context, isManualStop: Boolean = false) {
withContext(ioDispatcher) {
if (isManualStop) onManualStop() if (isManualStop) onManualStop()
Timber.i("Stopping vpn service") Timber.i("Stopping vpn service")
actionOnService( actionOnService(
@ -64,6 +76,7 @@ class ServiceManager(private val appDataRepository: AppDataRepository) {
WireGuardTunnelService::class.java, WireGuardTunnelService::class.java,
) )
} }
}
private suspend fun onManualStop() { private suspend fun onManualStop() {
appDataRepository.appState.setManualStop() appDataRepository.appState.setManualStop()
@ -80,6 +93,7 @@ class ServiceManager(private val appDataRepository: AppDataRepository) {
tunnelId: Int? = null, tunnelId: Int? = null,
isManualStart: Boolean = false isManualStart: Boolean = false
) { ) {
withContext(ioDispatcher) {
if (isManualStart) onManualStart(tunnelId) if (isManualStart) onManualStart(tunnelId)
actionOnService( actionOnService(
Action.START_FOREGROUND, Action.START_FOREGROUND,
@ -88,6 +102,7 @@ class ServiceManager(private val appDataRepository: AppDataRepository) {
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) }, tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
) )
} }
}
fun startWatcherServiceForeground( fun startWatcherServiceForeground(
context: Context, context: Context,

View File

@ -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 &&

View File

@ -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,6 +193,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
private suspend fun watchForMobileDataConnectivityChanges() { private suspend fun watchForMobileDataConnectivityChanges() {
withContext(ioDispatcher) {
mobileDataService.networkStatus.collect { status -> mobileDataService.networkStatus.collect { status ->
when (status) { when (status) {
is NetworkStatus.Available -> { is NetworkStatus.Available -> {
@ -213,8 +225,11 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
} }
} }
}
private suspend fun watchForPingFailure() { private suspend fun watchForPingFailure() {
val context = this
withContext(ioDispatcher) {
try { try {
do { do {
if (vpnService.vpnState.value.status == TunnelState.UP) { if (vpnService.vpnState.value.status == TunnelState.UP) {
@ -234,9 +249,9 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
if (results.contains(false)) { if (results.contains(false)) {
Timber.i("Restarting VPN for ping failure") Timber.i("Restarting VPN for ping failure")
serviceManager.stopVpnServiceForeground(this) serviceManager.stopVpnServiceForeground(context)
delay(Constants.VPN_RESTART_DELAY) delay(Constants.VPN_RESTART_DELAY)
serviceManager.startVpnServiceForeground(this, it.id) serviceManager.startVpnServiceForeground(context, it.id)
delay(Constants.PING_COOLDOWN) delay(Constants.PING_COOLDOWN)
} }
} }
@ -247,6 +262,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
Timber.e(e) Timber.e(e)
} }
} }
}
private suspend fun watchForSettingsChanges() { private suspend fun watchForSettingsChanges() {
appDataRepository.settings.getSettingsFlow().collect { settings -> appDataRepository.settings.getSettingsFlow().collect { settings ->
@ -265,6 +281,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
private suspend fun watchForEthernetConnectivityChanges() { private suspend fun watchForEthernetConnectivityChanges() {
withContext(ioDispatcher) {
ethernetService.networkStatus.collect { status -> ethernetService.networkStatus.collect { status ->
when (status) { when (status) {
is NetworkStatus.Available -> { is NetworkStatus.Available -> {
@ -296,8 +313,10 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
} }
} }
}
private suspend fun watchForWifiConnectivityChanges() { private suspend fun watchForWifiConnectivityChanges() {
withContext(ioDispatcher) {
wifiService.networkStatus.collect { status -> wifiService.networkStatus.collect { status ->
when (status) { when (status) {
is NetworkStatus.Available -> { is NetworkStatus.Available -> {
@ -308,6 +327,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
) )
} }
} }
is NetworkStatus.CapabilitiesChanged -> { is NetworkStatus.CapabilitiesChanged -> {
Timber.i("Wifi capabilities changed") Timber.i("Wifi capabilities changed")
networkEventsFlow.update { networkEventsFlow.update {
@ -317,7 +337,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
val ssid = wifiService.getNetworkName(status.networkCapabilities) val ssid = wifiService.getNetworkName(status.networkCapabilities)
ssid?.let { name -> ssid?.let { name ->
if(name.contains(Constants.UNREADABLE_SSID)) { if (name.contains(Constants.UNREADABLE_SSID)) {
Timber.w("SSID unreadable: missing permissions") Timber.w("SSID unreadable: missing permissions")
} else Timber.i("Detected valid SSID") } else Timber.i("Detected valid SSID")
appDataRepository.appState.setCurrentSsid(name) appDataRepository.appState.setCurrentSsid(name)
@ -340,6 +360,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
} }
} }
}
private suspend fun getMobileDataTunnel(): TunnelConfig? { private suspend fun getMobileDataTunnel(): TunnelConfig? {
return appDataRepository.tunnels.findByMobileDataTunnel().firstOrNull() return appDataRepository.tunnels.findByMobileDataTunnel().firstOrNull()
@ -349,11 +370,13 @@ 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() {
val context = this
withContext(ioDispatcher) {
networkEventsFlow.collectLatest { watcherState -> networkEventsFlow.collectLatest { watcherState ->
val autoTunnel = "Auto-tunnel watcher" val autoTunnel = "Auto-tunnel watcher"
if (!watcherState.settings.isAutoTunnelPaused) { if (!watcherState.settings.isAutoTunnelPaused) {
@ -363,42 +386,50 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
when { when {
watcherState.isEthernetConditionMet() -> { watcherState.isEthernetConditionMet() -> {
Timber.i("$autoTunnel - tunnel on on ethernet condition met") Timber.i("$autoTunnel - tunnel on on ethernet condition met")
if(isTunnelDown()) serviceManager.startVpnServiceForeground(this) 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,
Timber.i("$autoTunnel - tunnel connected on mobile data is not preferred condition met, switching to preferred") )
if(isTunnelDown()) serviceManager.startVpnServiceForeground( if (tunnelConfig?.isMobileDataTunnel == false && mobileDataTunnel != null) {
this, Timber.i("$autoTunnel - tunnel connected on mobile data is not preferred condition met, switching to preferred")
getMobileDataTunnel()?.id, serviceManager.startVpnServiceForeground(
context,
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() -> { watcherState.isUntrustedWifiConditionMet() -> {
if(tunnelConfig?.tunnelNetworks?.contains(watcherState.currentNetworkSSID) == false || if (tunnelConfig?.tunnelNetworks?.contains(watcherState.currentNetworkSSID) == false ||
tunnelConfig == null) { tunnelConfig == null) {
Timber.i("$autoTunnel - tunnel on ssid not associated with current tunnel condition met") Timber.i("$autoTunnel - tunnel on ssid not associated with current tunnel condition met")
getSsidTunnel(watcherState.currentNetworkSSID)?.let { getSsidTunnel(watcherState.currentNetworkSSID)?.let {
Timber.i("Found tunnel associated with this SSID, bringing tunnel up") Timber.i("Found tunnel associated with this SSID, bringing tunnel up")
if(isTunnelDown()) serviceManager.startVpnServiceForeground(this, it.id) if (isTunnelDown()) serviceManager.startVpnServiceForeground(
context,
it.id,
)
} ?: suspend { } ?: suspend {
Timber.i("No tunnel associated with this SSID, using defaults") Timber.i("No tunnel associated with this SSID, using defaults")
if (appDataRepository.getPrimaryOrFirstTunnel()?.name != vpnService.name) { val default = appDataRepository.getPrimaryOrFirstTunnel()
if(isTunnelDown()) serviceManager.startVpnServiceForeground(this) if (default?.name != vpnService.name) {
default?.let {
serviceManager.startVpnServiceForeground(context, it.id)
}
} }
}.invoke() }.invoke()
} }
@ -406,17 +437,17 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
watcherState.isTrustedWifiConditionMet() -> { watcherState.isTrustedWifiConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on trusted wifi condition 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)
} }
watcherState.isTunnelOffOnWifiConditionMet() -> { watcherState.isTunnelOffOnWifiConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off") Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off")
if(!isTunnelDown()) serviceManager.stopVpnServiceForeground(this) if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
} }
watcherState.isTunnelOffOnNoConnectivityMet() -> { watcherState.isTunnelOffOnNoConnectivityMet() -> {
Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off") Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off")
if(!isTunnelDown()) serviceManager.stopVpnServiceForeground(this) if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
} }
else -> { else -> {
@ -426,4 +457,5 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
} }
} }
}
} }

View File

@ -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,6 +86,7 @@ class WireGuardTunnelService : ForegroundService() {
//TODO improve tunnel notifications //TODO improve tunnel notifications
private suspend fun handshakeNotifications() { private suspend fun handshakeNotifications() {
withContext(ioDispatcher) {
var tunnelName: String? = null var tunnelName: String? = null
vpnService.vpnState.collect { state -> vpnService.vpnState.collect { state ->
state.statistics state.statistics
@ -111,6 +123,7 @@ class WireGuardTunnelService : ForegroundService() {
} }
} }
} }
}
private fun launchAlwaysOnDisabledNotification() { private fun launchAlwaysOnDisabledNotification() {
launchVpnNotification( launchVpnNotification(
@ -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")
} }
} }

View File

@ -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)) {

View File

@ -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,17 +24,19 @@ 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 {
@ -51,27 +52,16 @@ class AutoTunnelControlTile : TileService() {
} }
} }
} }
}
override fun onTileAdded() { override fun onTileAdded() {
super.onTileAdded() super.onTileAdded()
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) {

View File

@ -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(

View File

@ -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

View File

@ -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,7 +96,8 @@ constructor(
} }
override suspend fun startTunnel(tunnelConfig: TunnelConfig?): TunnelState { override suspend fun startTunnel(tunnelConfig: TunnelConfig?): TunnelState {
return try { return withContext(ioDispatcher) {
try {
//TODO we need better error handling here //TODO we need better error handling here
val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel() val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel()
if (config != null) { if (config != null) {
@ -104,8 +109,9 @@ constructor(
TunnelState.from(State.DOWN) 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,6 +140,7 @@ constructor(
} }
override suspend fun stopTunnel() { override suspend fun stopTunnel() {
withContext(ioDispatcher) {
try { try {
if (getState() == TunnelState.UP) { if (getState() == TunnelState.UP) {
val state = setState(null, TunnelState.DOWN) val state = setState(null, TunnelState.DOWN)
@ -146,9 +153,10 @@ constructor(
Timber.e("Failed to stop amnezia tunnel with error: ${e.message}") 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))
} }

View File

@ -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,
) )
} }
} }

View File

@ -8,11 +8,11 @@ abstract class TunnelStatistics {
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
} }

View File

@ -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,
) )
} }
} }

View File

@ -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(

View File

@ -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,10 +236,11 @@ 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}",
arguments =
listOf( listOf(
navArgument("id") { navArgument("id") {
type = NavType.StringType type = NavType.StringType
@ -249,18 +249,20 @@ class MainActivity : AppCompatActivity() {
navArgument("configType") { navArgument("configType") {
type = NavType.StringType type = NavType.StringType
defaultValue = ConfigType.WIREGUARD.name 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,
) )
} }
} }

View File

@ -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()
} }

View File

@ -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(
label, textAlign = TextAlign.Start,
modifier = Modifier
.weight( .weight(
weight = 1.0f, weight = 1.0f,
fill = false, fill = false,
), ),
softWrap = true) softWrap = true,
)
Switch( Switch(
modifier = modifier, modifier = modifier,
enabled = enabled, enabled = enabled,

View File

@ -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(),

View File

@ -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`)

View File

@ -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),
) )
} }
} }

View File

@ -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 "",
) )
} }
} }

View File

@ -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 =

View File

@ -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,18 +312,33 @@ 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 = {
@ -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,10 +444,23 @@ fun MainScreen(
flingBehavior = ScrollableDefaults.flingBehavior(), flingBehavior = ScrollableDefaults.flingBehavior(),
) { ) {
item { item {
AnimatedVisibility(
uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn(),
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.padding(top = 100.dp)
.fillMaxSize(),
) {
val gettingStarted = buildAnnotatedString { val gettingStarted = buildAnnotatedString {
append(stringResource(id = R.string.see_the)) append(stringResource(id = R.string.see_the))
append(" ") append(" ")
pushStringAnnotation(tag = "gettingStarted", annotation = stringResource(id = R.string.getting_started_url)) pushStringAnnotation(
tag = "gettingStarted",
annotation = stringResource(id = R.string.getting_started_url),
)
withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) { withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) {
append(stringResource(id = R.string.getting_started_guide)) append(stringResource(id = R.string.getting_started_guide))
} }
@ -444,20 +469,21 @@ fun MainScreen(
append(stringResource(R.string.unsure_how)) append(stringResource(R.string.unsure_how))
append(".") append(".")
} }
AnimatedVisibility( Text(
uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) { text = stringResource(R.string.no_tunnels),
Column( fontStyle = FontStyle.Italic,
horizontalAlignment = Alignment.CenterHorizontally, )
verticalArrangement = Arrangement.Center,
modifier = Modifier.padding(top = 100.dp)
) {
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
ClickableText( ClickableText(
modifier = Modifier.padding(vertical = 10.dp, horizontal = 24.dp), modifier = Modifier
.padding(vertical = 10.dp, horizontal = 24.dp),
text = gettingStarted, text = gettingStarted,
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center), style = MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
),
) { ) {
gettingStarted.getStringAnnotations(tag = "gettingStarted", it, it).firstOrNull()?.let { annotation -> gettingStarted.getStringAnnotations(tag = "gettingStarted", it, it)
.firstOrNull()?.let { annotation ->
appViewModel.openWebPage(annotation.item, context) 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) &&

View File

@ -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,14 +136,18 @@ constructor(
} }
suspend fun onTunnelQrResult(result: String, configType: ConfigType): Result<Unit> { suspend fun onTunnelQrResult(result: String, configType: ConfigType): Result<Unit> {
return try { return withContext(ioDispatcher) {
try {
validateConfigString(result, configType) validateConfigString(result, configType)
val tunnelName = makeTunnelNameUnique(generateQrCodeTunnelName(result, configType)) val tunnelName = makeTunnelNameUnique(generateQrCodeTunnelName(result, configType))
val tunnelConfig = when(configType) { val tunnelConfig = when (configType) {
ConfigType.AMNEZIA ->{ ConfigType.AMNEZIA -> {
TunnelConfig(name = tunnelName, amQuick = result, TunnelConfig(
wgQuick = TunnelConfig.configFromAmQuick(result).toWgQuickString()) 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) addTunnel(tunnelConfig)
@ -151,8 +157,10 @@ constructor(
Result.failure(WgTunnelExceptions.InvalidQrCode()) Result.failure(WgTunnelExceptions.InvalidQrCode())
} }
} }
}
private suspend fun makeTunnelNameUnique(name : String) : String { private suspend fun makeTunnelNameUnique(name: String): String {
return withContext(ioDispatcher) {
val tunnels = appDataRepository.tunnels.getAll() val tunnels = appDataRepository.tunnels.getAll()
var tunnelName = name var tunnelName = name
var num = 1 var num = 1
@ -160,39 +168,64 @@ constructor(
tunnelName = name + "(${num})" tunnelName = name + "(${num})"
num++ num++
} }
return tunnelName 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()
} }
} }
} }
viewModelScope.launch {
val tunnelName = makeTunnelNameUnique(getNameFromFileName(fileName)) val tunnelName = makeTunnelNameUnique(getNameFromFileName(fileName))
addTunnel(TunnelConfig(name = tunnelName, wgQuick = wgQuick, amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT)) 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,
configType: ConfigType,
context: Context
): Result<Unit> {
return withContext(ioDispatcher) {
try {
if (isValidUriContentScheme(uri)) { if (isValidUriContentScheme(uri)) {
val fileName = getFileName(context, uri) val fileName = getFileName(context, uri)
return when (getFileExtensionFromFileName(fileName)) { return@withContext when (getFileExtensionFromFileName(fileName)) {
Constants.CONF_FILE_EXTENSION -> Constants.CONF_FILE_EXTENSION ->
saveTunnelFromConfUri(fileName, uri, configType, context) saveTunnelFromConfUri(fileName, uri, configType, context)
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri, configType, context)
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(
uri,
configType,
context,
)
else -> Result.failure(WgTunnelExceptions.InvalidFileExtension()) else -> Result.failure(WgTunnelExceptions.InvalidFileExtension())
} }
} else { } else {
@ -203,9 +236,15 @@ constructor(
Result.failure(WgTunnelExceptions.FileReadFailed()) 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,
configType: ConfigType,
context: Context
): Result<Unit> {
return withContext(ioDispatcher) {
ZipInputStream(getInputStreamFromUri(uri, context)).use { zip ->
generateSequence { zip.nextEntry } generateSequence { zip.nextEntry }
.filterNot { .filterNot {
it.isDirectory || it.isDirectory ||
@ -213,23 +252,30 @@ constructor(
} }
.forEach { .forEach {
val name = getNameFromFileName(it.name) val name = getNameFromFileName(it.name)
withContext(viewModelScope.coroutineContext + Dispatchers.IO) { withContext(viewModelScope.coroutineContext) {
try { try {
var amQuick : String? = null var amQuick: String? = null
val wgQuick = val wgQuick =
when(configType) { when (configType) {
ConfigType.AMNEZIA -> { ConfigType.AMNEZIA -> {
val config = org.amnezia.awg.config.Config.parse(zip) val config = org.amnezia.awg.config.Config.parse(zip)
amQuick = config.toAwgQuickString() amQuick = config.toAwgQuickString()
config.toWgQuickString() config.toWgQuickString()
} }
ConfigType.WIREGUARD -> { ConfigType.WIREGUARD -> {
Config.parse(zip).toWgQuickString() Config.parse(zip).toWgQuickString()
} }
} }
addTunnel(TunnelConfig(name = makeTunnelNameUnique(name), wgQuick = wgQuick, amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT)) addTunnel(
TunnelConfig(
name = makeTunnelNameUnique(name),
wgQuick = wgQuick,
amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT,
),
)
Result.success(Unit) Result.success(Unit)
} catch (e : Exception) { } catch (e: Exception) {
Result.failure(WgTunnelExceptions.FileReadFailed()) Result.failure(WgTunnelExceptions.FileReadFailed())
} }
} }
@ -237,18 +283,30 @@ constructor(
Result.success(Unit) Result.success(Unit)
} }
} }
}
private suspend fun saveTunnelFromConfUri(name: String, uri: Uri, configType: ConfigType, context: Context): Result<Unit> { private suspend fun saveTunnelFromConfUri(
name: String,
uri: Uri,
configType: ConfigType,
context: Context
): Result<Unit> {
return withContext(ioDispatcher) {
val stream = getInputStreamFromUri(uri, context) val stream = getInputStreamFromUri(uri, context)
return if (stream != null) { return@withContext if (stream != null) {
try {
saveTunnelConfigFromStream(stream, name, configType) saveTunnelConfigFromStream(stream, name, configType)
} catch (e: Exception) {
return@withContext Result.failure(WgTunnelExceptions.ConfigParseError())
}
Result.success(Unit) Result.success(Unit)
} else { } else {
Result.failure(WgTunnelExceptions.FileReadFailed()) Result.failure(WgTunnelExceptions.FileReadFailed())
} }
} }
}
private suspend fun addTunnel(tunnelConfig: TunnelConfig) { private fun addTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
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 {

View File

@ -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,28 +118,28 @@ 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 =
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) tvFobColor else secondaryColor
val fobIconColor =
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) Color.White else MaterialTheme.colorScheme.background
AnimatedVisibility(
visible = true,
enter = slideInVertically(initialOffsetY = { it * 2 }),
exit = slideOutVertically(targetOffsetY = { it * 2 }),
modifier = Modifier
.focusRequester(focusRequester)
.focusGroup(),
) {
MultiFloatingActionButton( MultiFloatingActionButton(
modifier =
(if (
WireGuardAutoTunnel.isRunningOnAndroidTv()
)
Modifier.focusRequester(focusRequester)
else Modifier)
.onFocusChanged {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
fobColor = if (it.isFocused) hoverColor else secondaryColor
}
},
fabIcon = FabIcon( fabIcon = FabIcon(
iconRes = R.drawable.edit, iconRes = R.drawable.edit,
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(
@ -144,18 +148,33 @@ fun OptionsScreen(
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.edit, icon = R.drawable.edit,
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.edit, icon = R.drawable.edit,
value = ConfigType.WIREGUARD.name value = ConfigType.WIREGUARD.name,
miniFabOption = FabOption(
backgroundTint = fobColor,
fobIconColor,
),
), ),
), ),
onFabItemClicked = { onFabItemClicked = {
@ -167,6 +186,7 @@ fun OptionsScreen(
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
) )
} }
},
) { ) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,

View File

@ -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) {

View File

@ -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,19 +135,23 @@ fun SettingsScreen(
} }
file file
} }
val amFiles = uiState.tunnels.mapNotNull { config -> if(config.amQuick != TunnelConfig.AM_QUICK_DEFAULT) { val amFiles = uiState.tunnels.mapNotNull { config ->
if (config.amQuick != TunnelConfig.AM_QUICK_DEFAULT) {
val file = File(context.cacheDir, "${config.name}-am.conf") val file = File(context.cacheDir, "${config.name}-am.conf")
file.outputStream().use { file.outputStream().use {
it.write(config.amQuick.toByteArray()) it.write(config.amQuick.toByteArray())
} }
file file
} else null } } else null
FileUtils.saveFilesToZip(context, wgFiles + amFiles).onFailure { }
scope.launch {
viewModel.onExportTunnels(wgFiles + amFiles).onFailure {
appViewModel.showSnackbarMessage(it.getMessage(context)) appViewModel.showSnackbarMessage(it.getMessage(context))
}.onSuccess { }.onSuccess {
didExportFiles = true didExportFiles = true
appViewModel.showSnackbarMessage(context.getString(R.string.exported_configs_message)) appViewModel.showSnackbarMessage(context.getString(R.string.exported_configs_message))
} }
}
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
} }
@ -190,12 +192,10 @@ 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() {
isBackgroundLocationGranted = isBackgroundLocationGranted =

View File

@ -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,
) ),
) )
} }

View File

@ -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))
} }

View File

@ -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,

View File

@ -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)
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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())
}
}
} }

View File

@ -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)
// }
// }
}

View File

@ -4,6 +4,6 @@
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>

View File

@ -4,6 +4,6 @@
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>

View File

@ -4,6 +4,6 @@
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>

View File

@ -5,5 +5,5 @@
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>

View File

@ -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"

View File

@ -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

View File

@ -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" }

View File

@ -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

View File

@ -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)
} }

View File

@ -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>
}

View File

@ -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 {
private var logcatReader: LogcatReader? = null
override fun start(onLogMessage: ((message: LogMessage) -> Unit)?) {
logcatReader ?: run {
logcatReader = LogcatReader(
LogcatHelperInit.pID.toString(),
LogcatHelperInit.logcatPath,
onLogMessage,
)
}
logcatReader?.let { logReader ->
if (!logReader.isAlive) logReader.start()
}
}
override fun stop() {
logcatReader?.stopLogs()
logcatReader = null
}
private fun mergeLogsApi26(sourceDir: String, outputFile: File) {
val outputFilePath = Paths.get(outputFile.absolutePath)
val logcatPath = Paths.get(sourceDir)
Files.list(logcatPath).use {
it.sorted { o1, o2 ->
Files.getLastModifiedTime(o1).compareTo(Files.getLastModifiedTime(o2))
}
.flatMap(Files::lines).use { lines ->
lines.forEach { line ->
Files.write(
outputFilePath,
(line + System.lineSeparator()).toByteArray(),
StandardOpenOption.CREATE,
StandardOpenOption.APPEND,
)
}
}
}
}
override suspend fun getLogFile(): Result<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 -> return findKeyRegex.replace(log, "<crypto-key>").let { first ->
findIpv6AddressRegex.replace(first, "<ipv6-address>").let { second -> findIpv6AddressRegex.replace(first, "<ipv6-address>").let { second ->
findTunnelNameRegex.replace(second, "<tunnel>") findTunnelNameRegex.replace(second, "<tunnel>")
} }
}.let{ last -> findIpv4AddressRegex.replace(last,"<ipv4-address>") } }.let { last -> findIpv4AddressRegex.replace(last, "<ipv4-address>") }
} }
fun clear() { override fun run() {
Runtime.getRuntime().exec("logcat -c") 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()
}
}
}
}
} }
} }

View File

@ -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(
Instant.now().toString(),
"0", "0",
"0", "0",
LogLevel.VERBOSE, LogLevel.VERBOSE,
"System", "System",
logcatLine, logcatLine,
) )
else { } else {
//TODO improve this // 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]),