mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8bbe81d294 | |||
| 571fb1b12c | |||
| 02f6f97aa1 | |||
| 1d74d0984e | |||
| 6448386f76 | |||
| d09e85ba45 | |||
| a9bc1cc7f0 | |||
| 54d9653f04 | |||
| efc66821a6 | |||
| 3af7adc45b | |||
| 5754f2183c | |||
| f7f7f1bd9d | |||
| 57bb3f5e74 | |||
| 49196e7c7b | |||
| 894b63e668 | |||
| e16d44ff20 | |||
| b8b3f3001b | |||
| d142ecea6e | |||
| b793984ede | |||
| ae2532afe5 | |||
| 2720a3b35e | |||
| 2350364543 | |||
| f4172cb1fc | |||
| 90c482ae4f | |||
| 1eb8ad62e0 |
@@ -4,7 +4,7 @@
|
||||
|
||||
We as individuals involved in this project, pledge to participate in this
|
||||
community in a respectful, constructive, and civil manner as we work towards a common goal
|
||||
of delivering free, open source, and value adding software for all.
|
||||
of delivering free, open source, and value adding software for all.
|
||||
|
||||
## Standard
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Issue Updates Workflow
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, closed, reopened]
|
||||
types: [ opened, closed, reopened ]
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Release Updates Workflow
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
types: [ published ]
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -22,7 +22,8 @@ WG Tunnel
|
||||
|
||||
<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)
|
||||
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.
|
||||
@@ -64,27 +65,27 @@ and on while on different networks. This app was created to offer a free solutio
|
||||
* Battery preservation measures
|
||||
* Restart tunnel on ping failure (beta)
|
||||
|
||||
## Fdroid
|
||||
|
||||
Want updates faster?
|
||||
|
||||
Check out my personal [fdroid repository](https://github.com/zaneschepke/fdroid) to get updates the moment they are released.
|
||||
|
||||
## Docs
|
||||
|
||||
Basic documentation of the feature and behaviors of this app can be
|
||||
found [here](https://zaneschepke.com/wgtunnel-docs/overview.html).
|
||||
Information about features, behaviors, and answers to common questions can be found in the app [documentation](https://zaneschepke.com/wgtunnel-docs/overview.html).
|
||||
|
||||
The repository for these docs can be found [here](https://github.com/zaneschepke/wgtunnel-docs).
|
||||
|
||||
## Contributing
|
||||
|
||||
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.
|
||||
|
||||
## 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/).\
|
||||
[](https://hosted.weblate.org/engage/wg-tunnel/)
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
@@ -98,4 +99,11 @@ And then build the app:
|
||||
$ ./gradlew assembleDebug
|
||||
```
|
||||
|
||||
</span>
|
||||
## Contributing
|
||||
|
||||
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.
|
||||
|
||||
@@ -203,7 +203,6 @@ dependencies {
|
||||
|
||||
// barcode scanning
|
||||
implementation(libs.zxing.android.embedded)
|
||||
implementation(libs.zxing.core)
|
||||
|
||||
// bio
|
||||
implementation(libs.androidx.biometric.ktx)
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||
<fields>;
|
||||
}
|
||||
}
|
||||
Vendored
+1
-3
@@ -21,6 +21,4 @@
|
||||
#-renamesourcefileattribute SourceFile
|
||||
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||
<fields>;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -51,7 +51,7 @@
|
||||
</queries>
|
||||
<application
|
||||
android:name=".WireGuardAutoTunnel"
|
||||
android:allowBackup="true"
|
||||
android:allowBackup="false"
|
||||
android:banner="@drawable/ic_banner"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
|
||||
@@ -3,35 +3,60 @@ package com.zaneschepke.wireguardautotunnel
|
||||
import android.app.Application
|
||||
import android.content.ComponentName
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.StrictMode
|
||||
import android.os.StrictMode.ThreadPolicy
|
||||
import android.service.quicksettings.TileService
|
||||
import com.zaneschepke.logcatter.LocalLogCollector
|
||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidApp
|
||||
class WireGuardAutoTunnel : Application() {
|
||||
|
||||
@Inject
|
||||
lateinit var localLogCollector: LocalLogCollector
|
||||
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
@Inject
|
||||
@IoDispatcher
|
||||
lateinit var ioDispatcher: CoroutineDispatcher
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
instance = this
|
||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) else Timber.plant(ReleaseTree())
|
||||
PinManager.initialize(this)
|
||||
}
|
||||
|
||||
override fun onLowMemory() {
|
||||
super.onLowMemory()
|
||||
applicationScope.cancel("onLowMemory() called by system")
|
||||
applicationScope = MainScope()
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
StrictMode.setThreadPolicy(
|
||||
ThreadPolicy.Builder()
|
||||
.detectDiskReads()
|
||||
.detectDiskWrites()
|
||||
.detectNetwork()
|
||||
.penaltyLog()
|
||||
.build(),
|
||||
)
|
||||
} else Timber.plant(ReleaseTree())
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
//TODO disable pin lock for now
|
||||
//PinManager.initialize(this@WireGuardAutoTunnel)
|
||||
if (!isRunningOnAndroidTv()) localLogCollector.start()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
var applicationScope = MainScope()
|
||||
|
||||
lateinit var instance: WireGuardAutoTunnel
|
||||
private set
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
to = 7,
|
||||
spec = RemoveLegacySettingColumnsMigration::class,
|
||||
),
|
||||
AutoMigration(7, 8)
|
||||
AutoMigration(7, 8),
|
||||
],
|
||||
exportSchema = true,
|
||||
)
|
||||
|
||||
@@ -21,7 +21,7 @@ interface TunnelConfigDao {
|
||||
suspend fun getById(id: Long): TunnelConfig?
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig WHERE name=:name")
|
||||
suspend fun getByName(name: String) : TunnelConfig?
|
||||
suspend fun getByName(name: String): TunnelConfig?
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig")
|
||||
suspend fun getAll(): TunnelConfigs
|
||||
@@ -36,10 +36,10 @@ interface TunnelConfigDao {
|
||||
suspend fun findByTunnelNetworkName(name: String): TunnelConfigs
|
||||
|
||||
@Query("UPDATE TunnelConfig SET is_primary_tunnel = 0 WHERE is_primary_tunnel =1")
|
||||
fun resetPrimaryTunnel()
|
||||
suspend fun resetPrimaryTunnel()
|
||||
|
||||
@Query("UPDATE TunnelConfig SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1")
|
||||
fun resetMobileDataTunnel()
|
||||
suspend fun resetMobileDataTunnel()
|
||||
|
||||
@Query("SELECT * FROM TUNNELCONFIG WHERE is_primary_tunnel=1")
|
||||
suspend fun findByPrimary(): TunnelConfigs
|
||||
|
||||
+28
-16
@@ -7,14 +7,20 @@ import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
|
||||
class DataStoreManager(private val context: Context) {
|
||||
class DataStoreManager(
|
||||
private val context: Context,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
||||
) {
|
||||
companion object {
|
||||
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
|
||||
val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
|
||||
@@ -32,20 +38,24 @@ class DataStoreManager(private val context: Context) {
|
||||
)
|
||||
|
||||
suspend fun init() {
|
||||
try {
|
||||
context.dataStore.data.first()
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
withContext(ioDispatcher) {
|
||||
try {
|
||||
context.dataStore.data.first()
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) {
|
||||
try {
|
||||
context.dataStore.edit { it[key] = value }
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
withContext(ioDispatcher) {
|
||||
try {
|
||||
context.dataStore.edit { it[key] = value }
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,11 +63,13 @@ class DataStoreManager(private val context: Context) {
|
||||
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] }
|
||||
|
||||
suspend fun <T> getFromStore(key: Preferences.Key<T>): T? {
|
||||
return try {
|
||||
context.dataStore.data.map { it[key] }.first()
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
null
|
||||
return withContext(ioDispatcher) {
|
||||
try {
|
||||
context.dataStore.data.map { it[key] }.first()
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,12 +40,14 @@ data class TunnelConfig(
|
||||
Config.parse(it)
|
||||
}
|
||||
}
|
||||
fun configFromAmQuick(amQuick: String) : org.amnezia.awg.config.Config {
|
||||
|
||||
fun configFromAmQuick(amQuick: String): org.amnezia.awg.config.Config {
|
||||
val inputStream: InputStream = amQuick.byteInputStream()
|
||||
return inputStream.bufferedReader(Charsets.UTF_8).use {
|
||||
org.amnezia.awg.config.Config.parse(it)
|
||||
}
|
||||
}
|
||||
|
||||
const val AM_QUICK_DEFAULT = ""
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ interface TunnelConfigRepository {
|
||||
|
||||
suspend fun count(): Int
|
||||
|
||||
suspend fun findByTunnelName(name : String) : TunnelConfig?
|
||||
suspend fun findByTunnelName(name: String): TunnelConfig?
|
||||
|
||||
suspend fun findByTunnelNetworksName(name: String): TunnelConfigs
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+4
@@ -2,6 +2,10 @@ package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class Kernel
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class Userspace
|
||||
@@ -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
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object CoroutinesDispatchersModule {
|
||||
@DefaultDispatcher
|
||||
@Provides
|
||||
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
|
||||
|
||||
@IoDispatcher
|
||||
@Provides
|
||||
fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
|
||||
|
||||
@MainDispatcher
|
||||
@Provides
|
||||
fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
|
||||
|
||||
@MainImmediateDispatcher
|
||||
@Provides
|
||||
fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class DatabaseModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
|
||||
return Room.databaseBuilder(
|
||||
context,
|
||||
AppDatabase::class.java,
|
||||
context.getString(R.string.db_name),
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.addCallback(DatabaseCallback())
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class Kernel
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
|
||||
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||
@@ -18,11 +21,25 @@ import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class RepositoryModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
|
||||
return Room.databaseBuilder(
|
||||
context,
|
||||
AppDatabase::class.java,
|
||||
context.getString(R.string.db_name),
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.addCallback(DatabaseCallback())
|
||||
.build()
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideSettingsDoa(appDatabase: AppDatabase): SettingsDao {
|
||||
@@ -49,8 +66,11 @@ class RepositoryModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providePreferencesDataStore(@ApplicationContext context: Context): DataStoreManager {
|
||||
return DataStoreManager(context)
|
||||
fun providePreferencesDataStore(
|
||||
@ApplicationContext context: Context,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher
|
||||
): DataStoreManager {
|
||||
return DataStoreManager(context, ioDispatcher)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -15,6 +15,9 @@ import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import javax.inject.Provider
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@@ -42,24 +45,36 @@ class TunnelModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAmneziaBackend(@ApplicationContext context: Context) : org.amnezia.awg.backend.Backend {
|
||||
fun provideAmneziaBackend(@ApplicationContext context: Context): org.amnezia.awg.backend.Backend {
|
||||
return org.amnezia.awg.backend.GoBackend(context)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideVpnService(
|
||||
amneziaBackend: org.amnezia.awg.backend.Backend,
|
||||
@Userspace userspaceBackend: Backend,
|
||||
@Kernel kernelBackend: Backend,
|
||||
appDataRepository: AppDataRepository
|
||||
amneziaBackend: Provider<org.amnezia.awg.backend.Backend>,
|
||||
@Userspace userspaceBackend: Provider<Backend>,
|
||||
@Kernel kernelBackend: Provider<Backend>,
|
||||
appDataRepository: AppDataRepository,
|
||||
@ApplicationScope applicationScope: CoroutineScope,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher
|
||||
): VpnService {
|
||||
return WireGuardTunnel(amneziaBackend,userspaceBackend, kernelBackend, appDataRepository)
|
||||
return WireGuardTunnel(
|
||||
amneziaBackend,
|
||||
userspaceBackend,
|
||||
kernelBackend,
|
||||
appDataRepository,
|
||||
applicationScope,
|
||||
ioDispatcher,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideServiceManager(appDataRepository: AppDataRepository): ServiceManager {
|
||||
return ServiceManager(appDataRepository)
|
||||
fun provideServiceManager(
|
||||
appDataRepository: AppDataRepository,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher
|
||||
): ServiceManager {
|
||||
return ServiceManager(appDataRepository, ioDispatcher)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+1
@@ -24,6 +24,7 @@ open class ForegroundService : LifecycleService() {
|
||||
when (action) {
|
||||
Action.START.name,
|
||||
Action.START_FOREGROUND.name -> startService(intent.extras)
|
||||
|
||||
Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService()
|
||||
Constants.ALWAYS_ON_VPN_ACTION -> {
|
||||
Timber.i("Always-on VPN starting service")
|
||||
|
||||
+38
-23
@@ -4,10 +4,16 @@ import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
class ServiceManager(private val appDataRepository: AppDataRepository) {
|
||||
class ServiceManager(
|
||||
private val appDataRepository: AppDataRepository,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
||||
) {
|
||||
|
||||
private fun <T : Service> actionOnService(
|
||||
action: Action,
|
||||
@@ -23,7 +29,10 @@ class ServiceManager(private val appDataRepository: AppDataRepository) {
|
||||
intent.component?.javaClass
|
||||
try {
|
||||
when (action) {
|
||||
Action.START_FOREGROUND, Action.STOP_FOREGROUND -> context.startForegroundService(intent)
|
||||
Action.START_FOREGROUND, Action.STOP_FOREGROUND -> context.startForegroundService(
|
||||
intent,
|
||||
)
|
||||
|
||||
Action.START, Action.STOP -> context.startService(intent)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -46,23 +55,27 @@ class ServiceManager(private val appDataRepository: AppDataRepository) {
|
||||
}
|
||||
|
||||
suspend fun stopVpnServiceForeground(context: Context, isManualStop: Boolean = false) {
|
||||
if (isManualStop) onManualStop()
|
||||
Timber.i("Stopping vpn service")
|
||||
actionOnService(
|
||||
Action.STOP_FOREGROUND,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
)
|
||||
withContext(ioDispatcher) {
|
||||
if (isManualStop) onManualStop()
|
||||
Timber.i("Stopping vpn service")
|
||||
actionOnService(
|
||||
Action.STOP_FOREGROUND,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun stopVpnService(context: Context, isManualStop: Boolean = false) {
|
||||
if (isManualStop) onManualStop()
|
||||
Timber.i("Stopping vpn service")
|
||||
actionOnService(
|
||||
Action.STOP,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
)
|
||||
withContext(ioDispatcher) {
|
||||
if (isManualStop) onManualStop()
|
||||
Timber.i("Stopping vpn service")
|
||||
actionOnService(
|
||||
Action.STOP,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onManualStop() {
|
||||
@@ -80,13 +93,15 @@ class ServiceManager(private val appDataRepository: AppDataRepository) {
|
||||
tunnelId: Int? = null,
|
||||
isManualStart: Boolean = false
|
||||
) {
|
||||
if (isManualStart) onManualStart(tunnelId)
|
||||
actionOnService(
|
||||
Action.START_FOREGROUND,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
|
||||
)
|
||||
withContext(ioDispatcher) {
|
||||
if (isManualStart) onManualStart(tunnelId)
|
||||
actionOnService(
|
||||
Action.START_FOREGROUND,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun startWatcherServiceForeground(
|
||||
|
||||
-7
@@ -21,13 +21,6 @@ data class WatcherState(
|
||||
isMobileDataConnected)
|
||||
}
|
||||
|
||||
fun isTunnelOnMobileDataPreferredConditionMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
settings.isTunnelOnMobileDataEnabled &&
|
||||
!isWifiConnected &&
|
||||
isMobileDataConnected)
|
||||
}
|
||||
|
||||
fun isTunnelOffOnMobileDataConditionMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
!settings.isTunnelOnMobileDataEnabled &&
|
||||
|
||||
+194
-167
@@ -8,6 +8,8 @@ import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
||||
@@ -19,13 +21,14 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.net.InetAddress
|
||||
import javax.inject.Inject
|
||||
@@ -56,6 +59,14 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
@Inject
|
||||
@IoDispatcher
|
||||
lateinit var ioDispatcher: CoroutineDispatcher
|
||||
|
||||
@Inject
|
||||
@MainImmediateDispatcher
|
||||
lateinit var mainImmediateDispatcher: CoroutineDispatcher
|
||||
|
||||
private val networkEventsFlow = MutableStateFlow(WatcherState())
|
||||
|
||||
private var watcherJob: Job? = null
|
||||
@@ -65,7 +76,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
lifecycleScope.launch(mainImmediateDispatcher) {
|
||||
try {
|
||||
if (appDataRepository.settings.getSettings().isAutoTunnelPaused) {
|
||||
launchWatcherPausedNotification()
|
||||
@@ -138,14 +149,14 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
private fun cancelWatcherJob() {
|
||||
try {
|
||||
watcherJob?.cancel()
|
||||
} catch (e : CancellationException) {
|
||||
} catch (e: CancellationException) {
|
||||
Timber.i("Watcher job cancelled")
|
||||
}
|
||||
}
|
||||
|
||||
private fun startWatcherJob() {
|
||||
watcherJob =
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
lifecycleScope.launch {
|
||||
val setting = appDataRepository.settings.getSettings()
|
||||
launch {
|
||||
Timber.i("Starting wifi watcher")
|
||||
@@ -182,69 +193,74 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
}
|
||||
|
||||
private suspend fun watchForMobileDataConnectivityChanges() {
|
||||
mobileDataService.networkStatus.collect { status ->
|
||||
when (status) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.i("Gained Mobile data connection")
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isMobileDataConnected = true,
|
||||
)
|
||||
withContext(ioDispatcher) {
|
||||
mobileDataService.networkStatus.collect { status ->
|
||||
when (status) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.i("Gained Mobile data connection")
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isMobileDataConnected = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isMobileDataConnected = true,
|
||||
)
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isMobileDataConnected = true,
|
||||
)
|
||||
}
|
||||
Timber.i("Mobile data capabilities changed")
|
||||
}
|
||||
Timber.i("Mobile data capabilities changed")
|
||||
}
|
||||
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isMobileDataConnected = false,
|
||||
)
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isMobileDataConnected = false,
|
||||
)
|
||||
}
|
||||
Timber.i("Lost mobile data connection")
|
||||
}
|
||||
Timber.i("Lost mobile data connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun watchForPingFailure() {
|
||||
try {
|
||||
do {
|
||||
if (vpnService.vpnState.value.status == TunnelState.UP) {
|
||||
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
|
||||
tunnelConfig?.let {
|
||||
val config = TunnelConfig.configFromWgQuick(it.wgQuick)
|
||||
val results = config.peers.map { peer ->
|
||||
val host = if (peer.endpoint.isPresent &&
|
||||
peer.endpoint.get().resolved.isPresent)
|
||||
peer.endpoint.get().resolved.get().host
|
||||
else Constants.DEFAULT_PING_IP
|
||||
Timber.i("Checking reachability of: $host")
|
||||
val reachable = InetAddress.getByName(host)
|
||||
.isReachable(Constants.PING_TIMEOUT.toInt())
|
||||
Timber.i("Result: reachable - $reachable")
|
||||
reachable
|
||||
}
|
||||
if (results.contains(false)) {
|
||||
Timber.i("Restarting VPN for ping failure")
|
||||
serviceManager.stopVpnServiceForeground(this)
|
||||
delay(Constants.VPN_RESTART_DELAY)
|
||||
serviceManager.startVpnServiceForeground(this, it.id)
|
||||
delay(Constants.PING_COOLDOWN)
|
||||
val context = this
|
||||
withContext(ioDispatcher) {
|
||||
try {
|
||||
do {
|
||||
if (vpnService.vpnState.value.status == TunnelState.UP) {
|
||||
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
|
||||
tunnelConfig?.let {
|
||||
val config = TunnelConfig.configFromWgQuick(it.wgQuick)
|
||||
val results = config.peers.map { peer ->
|
||||
val host = if (peer.endpoint.isPresent &&
|
||||
peer.endpoint.get().resolved.isPresent)
|
||||
peer.endpoint.get().resolved.get().host
|
||||
else Constants.DEFAULT_PING_IP
|
||||
Timber.i("Checking reachability of: $host")
|
||||
val reachable = InetAddress.getByName(host)
|
||||
.isReachable(Constants.PING_TIMEOUT.toInt())
|
||||
Timber.i("Result: reachable - $reachable")
|
||||
reachable
|
||||
}
|
||||
if (results.contains(false)) {
|
||||
Timber.i("Restarting VPN for ping failure")
|
||||
serviceManager.stopVpnServiceForeground(context)
|
||||
delay(Constants.VPN_RESTART_DELAY)
|
||||
serviceManager.startVpnServiceForeground(context, it.id)
|
||||
delay(Constants.PING_COOLDOWN)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
delay(Constants.PING_INTERVAL)
|
||||
} while (true)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
delay(Constants.PING_INTERVAL)
|
||||
} while (true)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,77 +281,82 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
}
|
||||
|
||||
private suspend fun watchForEthernetConnectivityChanges() {
|
||||
ethernetService.networkStatus.collect { status ->
|
||||
when (status) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.i("Gained Ethernet connection")
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isEthernetConnected = true,
|
||||
)
|
||||
withContext(ioDispatcher) {
|
||||
ethernetService.networkStatus.collect { status ->
|
||||
when (status) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.i("Gained Ethernet connection")
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isEthernetConnected = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.i("Ethernet capabilities changed")
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isEthernetConnected = true,
|
||||
)
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.i("Ethernet capabilities changed")
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isEthernetConnected = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isEthernetConnected = false,
|
||||
)
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isEthernetConnected = false,
|
||||
)
|
||||
}
|
||||
Timber.i("Lost Ethernet connection")
|
||||
}
|
||||
Timber.i("Lost Ethernet connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun watchForWifiConnectivityChanges() {
|
||||
wifiService.networkStatus.collect { status ->
|
||||
when (status) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.i("Gained Wi-Fi connection")
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isWifiConnected = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.i("Wifi capabilities changed")
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isWifiConnected = true,
|
||||
)
|
||||
}
|
||||
val ssid = wifiService.getNetworkName(status.networkCapabilities)
|
||||
ssid?.let { name ->
|
||||
if(name.contains(Constants.UNREADABLE_SSID)) {
|
||||
Timber.w("SSID unreadable: missing permissions")
|
||||
} else Timber.i("Detected valid SSID")
|
||||
appDataRepository.appState.setCurrentSsid(name)
|
||||
withContext(ioDispatcher) {
|
||||
wifiService.networkStatus.collect { status ->
|
||||
when (status) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.i("Gained Wi-Fi connection")
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
currentNetworkSSID = name,
|
||||
isWifiConnected = true,
|
||||
)
|
||||
}
|
||||
} ?: Timber.w("Failed to read ssid")
|
||||
}
|
||||
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isWifiConnected = false,
|
||||
)
|
||||
}
|
||||
Timber.i("Lost Wi-Fi connection")
|
||||
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.i("Wifi capabilities changed")
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isWifiConnected = true,
|
||||
)
|
||||
}
|
||||
val ssid = wifiService.getNetworkName(status.networkCapabilities)
|
||||
ssid?.let { name ->
|
||||
if (name.contains(Constants.UNREADABLE_SSID)) {
|
||||
Timber.w("SSID unreadable: missing permissions")
|
||||
} else Timber.i("Detected valid SSID")
|
||||
appDataRepository.appState.setCurrentSsid(name)
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
currentNetworkSSID = name,
|
||||
)
|
||||
}
|
||||
} ?: Timber.w("Failed to read ssid")
|
||||
}
|
||||
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isWifiConnected = false,
|
||||
)
|
||||
}
|
||||
Timber.i("Lost Wi-Fi connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -349,78 +370,84 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
return appDataRepository.tunnels.findByTunnelNetworksName(ssid).firstOrNull()
|
||||
}
|
||||
|
||||
private fun isTunnelDown() : Boolean {
|
||||
private fun isTunnelDown(): Boolean {
|
||||
return vpnService.vpnState.value.status == TunnelState.DOWN
|
||||
}
|
||||
|
||||
private suspend fun manageVpn() {
|
||||
networkEventsFlow.collectLatest { watcherState ->
|
||||
val autoTunnel = "Auto-tunnel watcher"
|
||||
if (!watcherState.settings.isAutoTunnelPaused) {
|
||||
//delay for rapid network state changes and then collect latest
|
||||
delay(Constants.WATCHER_COLLECTION_DELAY)
|
||||
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
|
||||
when {
|
||||
watcherState.isEthernetConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
|
||||
if(isTunnelDown()) serviceManager.startVpnServiceForeground(this)
|
||||
}
|
||||
val context = this
|
||||
withContext(ioDispatcher) {
|
||||
networkEventsFlow.collectLatest { watcherState ->
|
||||
val autoTunnel = "Auto-tunnel watcher"
|
||||
if (!watcherState.settings.isAutoTunnelPaused) {
|
||||
//delay for rapid network state changes and then collect latest
|
||||
delay(Constants.WATCHER_COLLECTION_DELAY)
|
||||
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
|
||||
when {
|
||||
watcherState.isEthernetConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
|
||||
if (isTunnelDown()) serviceManager.startVpnServiceForeground(context)
|
||||
}
|
||||
|
||||
watcherState.isMobileDataConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel on on mobile data condition met")
|
||||
if(isTunnelDown()) serviceManager.startVpnServiceForeground(this, getMobileDataTunnel()?.id)
|
||||
}
|
||||
|
||||
watcherState.isTunnelOnMobileDataPreferredConditionMet() -> {
|
||||
if(tunnelConfig?.isMobileDataTunnel == false) {
|
||||
getMobileDataTunnel()?.let {
|
||||
Timber.i("$autoTunnel - tunnel connected on mobile data is not preferred condition met, switching to preferred")
|
||||
if(isTunnelDown()) serviceManager.startVpnServiceForeground(
|
||||
this,
|
||||
getMobileDataTunnel()?.id,
|
||||
watcherState.isMobileDataConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel on mobile data condition met")
|
||||
val mobileDataTunnel = getMobileDataTunnel()
|
||||
val tunnel =
|
||||
mobileDataTunnel ?: appDataRepository.getPrimaryOrFirstTunnel()
|
||||
if (isTunnelDown() || tunnelConfig?.isMobileDataTunnel == false) {
|
||||
serviceManager.startVpnServiceForeground(
|
||||
context,
|
||||
tunnel?.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
|
||||
if(!isTunnelDown()) serviceManager.stopVpnServiceForeground(this)
|
||||
}
|
||||
|
||||
watcherState.isUntrustedWifiConditionMet() -> {
|
||||
if(tunnelConfig?.tunnelNetworks?.contains(watcherState.currentNetworkSSID) == false ||
|
||||
tunnelConfig == null) {
|
||||
Timber.i("$autoTunnel - tunnel on ssid not associated with current tunnel condition met")
|
||||
getSsidTunnel(watcherState.currentNetworkSSID)?.let {
|
||||
Timber.i("Found tunnel associated with this SSID, bringing tunnel up")
|
||||
if(isTunnelDown()) serviceManager.startVpnServiceForeground(this, it.id)
|
||||
} ?: suspend {
|
||||
Timber.i("No tunnel associated with this SSID, using defaults")
|
||||
if (appDataRepository.getPrimaryOrFirstTunnel()?.name != vpnService.name) {
|
||||
if(isTunnelDown()) serviceManager.startVpnServiceForeground(this)
|
||||
}
|
||||
}.invoke()
|
||||
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
|
||||
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
|
||||
}
|
||||
}
|
||||
|
||||
watcherState.isTrustedWifiConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off")
|
||||
if(!isTunnelDown()) serviceManager.stopVpnServiceForeground(this)
|
||||
}
|
||||
watcherState.isUntrustedWifiConditionMet() -> {
|
||||
if (tunnelConfig?.tunnelNetworks?.contains(watcherState.currentNetworkSSID) == false ||
|
||||
tunnelConfig == null) {
|
||||
Timber.i("$autoTunnel - tunnel on ssid not associated with current tunnel condition met")
|
||||
getSsidTunnel(watcherState.currentNetworkSSID)?.let {
|
||||
Timber.i("Found tunnel associated with this SSID, bringing tunnel up: ${it.name}")
|
||||
if (isTunnelDown() || tunnelConfig?.id != it.id) serviceManager.startVpnServiceForeground(
|
||||
context,
|
||||
it.id,
|
||||
)
|
||||
} ?: suspend {
|
||||
Timber.i("No tunnel associated with this SSID, using defaults")
|
||||
val default = appDataRepository.getPrimaryOrFirstTunnel()
|
||||
if (default?.name != vpnService.name) {
|
||||
default?.let {
|
||||
serviceManager.startVpnServiceForeground(context, it.id)
|
||||
}
|
||||
|
||||
watcherState.isTunnelOffOnWifiConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off")
|
||||
if(!isTunnelDown()) serviceManager.stopVpnServiceForeground(this)
|
||||
}
|
||||
}
|
||||
}.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
watcherState.isTunnelOffOnNoConnectivityMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off")
|
||||
if(!isTunnelDown()) serviceManager.stopVpnServiceForeground(this)
|
||||
}
|
||||
watcherState.isTrustedWifiConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off")
|
||||
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Timber.i("$autoTunnel - no condition met")
|
||||
watcherState.isTunnelOffOnWifiConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off")
|
||||
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
|
||||
}
|
||||
|
||||
watcherState.isTunnelOffOnNoConnectivityMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off")
|
||||
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Timber.i("$autoTunnel - no condition met")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+45
-32
@@ -7,6 +7,8 @@ import androidx.core.app.ServiceCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
|
||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||
@@ -17,10 +19,11 @@ import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -37,13 +40,21 @@ class WireGuardTunnelService : ForegroundService() {
|
||||
@Inject
|
||||
lateinit var notificationService: NotificationService
|
||||
|
||||
@Inject
|
||||
@MainImmediateDispatcher
|
||||
lateinit var mainImmediateDispatcher: CoroutineDispatcher
|
||||
|
||||
@Inject
|
||||
@IoDispatcher
|
||||
lateinit var ioDispatcher: CoroutineDispatcher
|
||||
|
||||
private var job: Job? = null
|
||||
|
||||
private var didShowConnected = false
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
lifecycleScope.launch(mainImmediateDispatcher) {
|
||||
//TODO fix this to not launch if AOVPN
|
||||
if (appDataRepository.tunnels.count() != 0) {
|
||||
launchVpnNotification()
|
||||
@@ -55,7 +66,7 @@ class WireGuardTunnelService : ForegroundService() {
|
||||
super.startService(extras)
|
||||
cancelJob()
|
||||
job =
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
lifecycleScope.launch {
|
||||
launch {
|
||||
val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY)
|
||||
if (vpnService.getState() == TunnelState.UP) {
|
||||
@@ -75,39 +86,41 @@ class WireGuardTunnelService : ForegroundService() {
|
||||
|
||||
//TODO improve tunnel notifications
|
||||
private suspend fun handshakeNotifications() {
|
||||
var tunnelName: String? = null
|
||||
vpnService.vpnState.collect { state ->
|
||||
withContext(ioDispatcher) {
|
||||
var tunnelName: String? = null
|
||||
vpnService.vpnState.collect { state ->
|
||||
state.statistics
|
||||
?.mapPeerStats()
|
||||
?.map { it.value?.handshakeStatus() }
|
||||
.let { statuses ->
|
||||
when {
|
||||
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
|
||||
if (!didShowConnected) {
|
||||
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
|
||||
tunnelName = state.tunnelConfig?.name
|
||||
launchVpnNotification(
|
||||
getString(R.string.tunnel_start_title),
|
||||
"${getString(R.string.tunnel_start_text)} - $tunnelName",
|
||||
)
|
||||
didShowConnected = true
|
||||
?.mapPeerStats()
|
||||
?.map { it.value?.handshakeStatus() }
|
||||
.let { statuses ->
|
||||
when {
|
||||
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
|
||||
if (!didShowConnected) {
|
||||
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
|
||||
tunnelName = state.tunnelConfig?.name
|
||||
launchVpnNotification(
|
||||
getString(R.string.tunnel_start_title),
|
||||
"${getString(R.string.tunnel_start_text)} - $tunnelName",
|
||||
)
|
||||
didShowConnected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
|
||||
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
|
||||
true -> {
|
||||
}
|
||||
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
|
||||
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
|
||||
true -> {
|
||||
}
|
||||
|
||||
else -> {}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
if (state.status == TunnelState.UP && state.tunnelConfig?.name != tunnelName) {
|
||||
tunnelName = state.tunnelConfig?.name
|
||||
launchVpnNotification(
|
||||
getString(R.string.tunnel_start_title),
|
||||
"${getString(R.string.tunnel_start_text)} - $tunnelName",
|
||||
)
|
||||
}
|
||||
if (state.status == TunnelState.UP && state.tunnelConfig?.name != tunnelName) {
|
||||
tunnelName = state.tunnelConfig?.name
|
||||
launchVpnNotification(
|
||||
getString(R.string.tunnel_start_title),
|
||||
"${getString(R.string.tunnel_start_text)} - $tunnelName",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,7 +134,7 @@ class WireGuardTunnelService : ForegroundService() {
|
||||
|
||||
override fun stopService() {
|
||||
super.stopService()
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
lifecycleScope.launch {
|
||||
vpnService.stopTunnel()
|
||||
didShowConnected = false
|
||||
}
|
||||
@@ -181,7 +194,7 @@ class WireGuardTunnelService : ForegroundService() {
|
||||
private fun cancelJob() {
|
||||
try {
|
||||
job?.cancel()
|
||||
} catch (e : CancellationException) {
|
||||
} catch (e: CancellationException) {
|
||||
Timber.i("Tunnel job cancelled")
|
||||
}
|
||||
}
|
||||
|
||||
+7
-3
@@ -2,14 +2,14 @@ package com.zaneschepke.wireguardautotunnel.service.shortcut
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -22,9 +22,13 @@ class ShortcutsActivity : ComponentActivity() {
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
WireGuardAutoTunnel.applicationScope.launch(Dispatchers.IO) {
|
||||
applicationScope.launch {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
if (settings.isShortcutsEnabled) {
|
||||
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
|
||||
|
||||
+19
-29
@@ -6,12 +6,11 @@ import android.service.quicksettings.TileService
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
@@ -25,29 +24,30 @@ class AutoTunnelControlTile : TileService() {
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
private var manualStartConfig: TunnelConfig? = null
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
scope.launch {
|
||||
appDataRepository.settings.getSettingsFlow().collectLatest {
|
||||
when (it.isAutoTunnelEnabled) {
|
||||
true -> {
|
||||
if (it.isAutoTunnelPaused) {
|
||||
setInactive()
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.paused))
|
||||
} else {
|
||||
setActive()
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.active))
|
||||
}
|
||||
applicationScope.launch {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
when (settings.isAutoTunnelEnabled) {
|
||||
true -> {
|
||||
if (settings.isAutoTunnelPaused) {
|
||||
setInactive()
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.paused))
|
||||
} else {
|
||||
setActive()
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.active))
|
||||
}
|
||||
}
|
||||
|
||||
false -> {
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled))
|
||||
setUnavailable()
|
||||
}
|
||||
false -> {
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled))
|
||||
setUnavailable()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,20 +58,10 @@ class AutoTunnelControlTile : TileService() {
|
||||
onStartListening()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
override fun onTileRemoved() {
|
||||
super.onTileRemoved()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
unlockAndRun {
|
||||
scope.launch {
|
||||
applicationScope.launch {
|
||||
try {
|
||||
appDataRepository.toggleWatcherServicePause()
|
||||
} catch (e: Exception) {
|
||||
|
||||
+11
-14
@@ -5,12 +5,13 @@ import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
@@ -28,14 +29,19 @@ class TunnelControlTile : TileService() {
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
private var manualStartConfig: TunnelConfig? = null
|
||||
|
||||
private var job: Job? = null;
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
Timber.d("On start listening called")
|
||||
scope.launch {
|
||||
//TODO Fix this
|
||||
if (job == null || job?.isCancelled == true) job = applicationScope.launch {
|
||||
vpnService.vpnState.collect { it ->
|
||||
when (it.status) {
|
||||
TunnelState.UP -> {
|
||||
@@ -52,22 +58,13 @@ class TunnelControlTile : TileService() {
|
||||
setTileDescription(it.name)
|
||||
} ?: setUnavailable()
|
||||
}
|
||||
|
||||
else -> setInactive()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
override fun onTileRemoved() {
|
||||
super.onTileRemoved()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
override fun onTileAdded() {
|
||||
super.onTileAdded()
|
||||
onStartListening()
|
||||
@@ -76,7 +73,7 @@ class TunnelControlTile : TileService() {
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
unlockAndRun {
|
||||
scope.launch {
|
||||
applicationScope.launch {
|
||||
try {
|
||||
if (vpnService.getState() == TunnelState.UP) {
|
||||
serviceManager.stopVpnServiceForeground(
|
||||
|
||||
@@ -7,16 +7,16 @@ enum class TunnelState {
|
||||
DOWN,
|
||||
TOGGLE;
|
||||
|
||||
fun toWgState() : Tunnel.State {
|
||||
return when(this) {
|
||||
fun toWgState(): Tunnel.State {
|
||||
return when (this) {
|
||||
UP -> Tunnel.State.UP
|
||||
DOWN -> Tunnel.State.DOWN
|
||||
TOGGLE -> Tunnel.State.TOGGLE
|
||||
}
|
||||
}
|
||||
|
||||
fun toAmState() : org.amnezia.awg.backend.Tunnel.State {
|
||||
return when(this) {
|
||||
fun toAmState(): org.amnezia.awg.backend.Tunnel.State {
|
||||
return when (this) {
|
||||
UP -> org.amnezia.awg.backend.Tunnel.State.UP
|
||||
DOWN -> org.amnezia.awg.backend.Tunnel.State.DOWN
|
||||
TOGGLE -> org.amnezia.awg.backend.Tunnel.State.TOGGLE
|
||||
@@ -24,15 +24,16 @@ enum class TunnelState {
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(state: Tunnel.State) : TunnelState {
|
||||
return when(state) {
|
||||
fun from(state: Tunnel.State): TunnelState {
|
||||
return when (state) {
|
||||
Tunnel.State.DOWN -> DOWN
|
||||
Tunnel.State.TOGGLE -> TOGGLE
|
||||
Tunnel.State.UP -> UP
|
||||
}
|
||||
}
|
||||
fun from(state: org.amnezia.awg.backend.Tunnel.State) : TunnelState {
|
||||
return when(state) {
|
||||
|
||||
fun from(state: org.amnezia.awg.backend.Tunnel.State): TunnelState {
|
||||
return when (state) {
|
||||
org.amnezia.awg.backend.Tunnel.State.DOWN -> DOWN
|
||||
org.amnezia.awg.backend.Tunnel.State.TOGGLE -> TOGGLE
|
||||
org.amnezia.awg.backend.Tunnel.State.UP -> UP
|
||||
|
||||
+67
-50
@@ -6,6 +6,8 @@ import com.wireguard.android.backend.Tunnel.State
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.module.Kernel
|
||||
import com.zaneschepke.wireguardautotunnel.module.Userspace
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics
|
||||
@@ -13,54 +15,58 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStati
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.amnezia.awg.backend.Tunnel
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
class WireGuardTunnel
|
||||
@Inject
|
||||
constructor(
|
||||
private val userspaceAmneziaBackend : org.amnezia.awg.backend.Backend,
|
||||
@Userspace private val userspaceBackend: Backend,
|
||||
@Kernel private val kernelBackend: Backend,
|
||||
private val userspaceAmneziaBackend: Provider<org.amnezia.awg.backend.Backend>,
|
||||
@Userspace private val userspaceBackend: Provider<Backend>,
|
||||
@Kernel private val kernelBackend: Provider<Backend>,
|
||||
private val appDataRepository: AppDataRepository,
|
||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
||||
) : VpnService {
|
||||
private val _vpnState = MutableStateFlow(VpnState())
|
||||
override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow()
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
private var statsJob: Job? = null
|
||||
|
||||
private var backend: Backend = userspaceBackend
|
||||
private lateinit var backend: Backend;
|
||||
|
||||
private var backendIsWgUserspace = true
|
||||
|
||||
private var backendIsAmneziaUserspace = false
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
backend = userspaceBackend.get()
|
||||
appDataRepository.settings.getSettingsFlow().collect {
|
||||
if (it.isKernelEnabled && (backendIsWgUserspace || backendIsAmneziaUserspace)) {
|
||||
Timber.d("Setting kernel backend")
|
||||
backend = kernelBackend
|
||||
Timber.i("Setting kernel backend")
|
||||
backend = kernelBackend.get()
|
||||
backendIsWgUserspace = false
|
||||
backendIsAmneziaUserspace = false
|
||||
} else if (!it.isKernelEnabled && !it.isAmneziaEnabled && !backendIsWgUserspace) {
|
||||
Timber.d("Setting WireGuard userspace backend")
|
||||
backend = userspaceBackend
|
||||
Timber.i("Setting WireGuard userspace backend")
|
||||
backend = userspaceBackend.get()
|
||||
backendIsWgUserspace = true
|
||||
backendIsAmneziaUserspace = false
|
||||
} else if (it.isAmneziaEnabled && !backendIsAmneziaUserspace) {
|
||||
Timber.d("Setting Amnezia userspace backend")
|
||||
Timber.i("Setting Amnezia userspace backend")
|
||||
backendIsAmneziaUserspace = true
|
||||
backendIsWgUserspace = false
|
||||
}
|
||||
@@ -68,16 +74,17 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun setState(tunnelConfig: TunnelConfig?, tunnelState: TunnelState) : TunnelState {
|
||||
return if(backendIsAmneziaUserspace) {
|
||||
private fun setState(tunnelConfig: TunnelConfig?, tunnelState: TunnelState): TunnelState {
|
||||
return if (backendIsAmneziaUserspace) {
|
||||
Timber.i("Using Amnezia backend")
|
||||
val config = tunnelConfig?.let {
|
||||
if(it.amQuick != "") TunnelConfig.configFromAmQuick(it.amQuick) else {
|
||||
if (it.amQuick != "") TunnelConfig.configFromAmQuick(it.amQuick) else {
|
||||
Timber.w("Using backwards compatible wg config, amnezia specific config not found.")
|
||||
TunnelConfig.configFromAmQuick(it.wgQuick)
|
||||
}
|
||||
}
|
||||
val state = userspaceAmneziaBackend.setState(this, tunnelState.toAmState(), config)
|
||||
val state =
|
||||
userspaceAmneziaBackend.get().setState(this, tunnelState.toAmState(), config)
|
||||
TunnelState.from(state)
|
||||
} else {
|
||||
Timber.i("Using Wg backend")
|
||||
@@ -92,20 +99,22 @@ constructor(
|
||||
}
|
||||
|
||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig?): TunnelState {
|
||||
return try {
|
||||
//TODO we need better error handling here
|
||||
val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel()
|
||||
if (config != null) {
|
||||
emitTunnelConfig(config)
|
||||
setState(config, TunnelState.UP)
|
||||
} else throw Exception("No tunnels")
|
||||
} catch (e: BackendException) {
|
||||
Timber.e("Failed to start tunnel with error: ${e.message}")
|
||||
TunnelState.from(State.DOWN)
|
||||
return withContext(ioDispatcher) {
|
||||
try {
|
||||
//TODO we need better error handling here
|
||||
val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel()
|
||||
if (config != null) {
|
||||
emitTunnelConfig(config)
|
||||
setState(config, TunnelState.UP)
|
||||
} else throw Exception("No tunnels")
|
||||
} catch (e: BackendException) {
|
||||
Timber.e("Failed to start tunnel with error: ${e.message}")
|
||||
TunnelState.from(State.DOWN)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun emitTunnelState(state : TunnelState) {
|
||||
private fun emitTunnelState(state: TunnelState) {
|
||||
_vpnState.tryEmit(
|
||||
_vpnState.value.copy(
|
||||
status = state,
|
||||
@@ -134,21 +143,25 @@ constructor(
|
||||
}
|
||||
|
||||
override suspend fun stopTunnel() {
|
||||
try {
|
||||
if (getState() == TunnelState.UP) {
|
||||
val state = setState(null, TunnelState.DOWN)
|
||||
resetVpnState()
|
||||
emitTunnelState(state)
|
||||
withContext(ioDispatcher) {
|
||||
try {
|
||||
if (getState() == TunnelState.UP) {
|
||||
val state = setState(null, TunnelState.DOWN)
|
||||
resetVpnState()
|
||||
emitTunnelState(state)
|
||||
}
|
||||
} catch (e: BackendException) {
|
||||
Timber.e("Failed to stop wireguard tunnel with error: ${e.message}")
|
||||
} catch (e: org.amnezia.awg.backend.BackendException) {
|
||||
Timber.e("Failed to stop amnezia tunnel with error: ${e.message}")
|
||||
}
|
||||
} catch (e: BackendException) {
|
||||
Timber.e("Failed to stop wireguard tunnel with error: ${e.message}")
|
||||
} catch (e: org.amnezia.awg.backend.BackendException) {
|
||||
Timber.e("Failed to stop amnezia tunnel with error: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getState(): TunnelState {
|
||||
return if(backendIsAmneziaUserspace) TunnelState.from(userspaceAmneziaBackend.getState(this))
|
||||
return if (backendIsAmneziaUserspace) TunnelState.from(
|
||||
userspaceAmneziaBackend.get().getState(this),
|
||||
)
|
||||
else TunnelState.from(backend.getState(this))
|
||||
}
|
||||
|
||||
@@ -162,31 +175,35 @@ constructor(
|
||||
}
|
||||
|
||||
private fun handleStateChange(state: TunnelState) {
|
||||
val tunnel = this
|
||||
emitTunnelState(state)
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
||||
if (state == TunnelState.UP) {
|
||||
statsJob =
|
||||
scope.launch {
|
||||
while (true) {
|
||||
if(backendIsAmneziaUserspace) {
|
||||
emitBackendStatistics(AmneziaStatistics(userspaceAmneziaBackend.getStatistics(tunnel)))
|
||||
} else {
|
||||
emitBackendStatistics(WireGuardStatistics(backend.getStatistics(tunnel)))
|
||||
}
|
||||
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
|
||||
}
|
||||
}
|
||||
statsJob = startTunnelStatisticsJob()
|
||||
}
|
||||
if (state == TunnelState.DOWN) {
|
||||
try {
|
||||
statsJob?.cancel()
|
||||
} catch (e : CancellationException) {
|
||||
} catch (e: CancellationException) {
|
||||
Timber.i("Stats job cancelled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startTunnelStatisticsJob() = applicationScope.launch(ioDispatcher) {
|
||||
while (true) {
|
||||
if (backendIsAmneziaUserspace) {
|
||||
emitBackendStatistics(
|
||||
AmneziaStatistics(
|
||||
userspaceAmneziaBackend.get().getStatistics(this@WireGuardTunnel),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
emitBackendStatistics(WireGuardStatistics(backend.getStatistics(this@WireGuardTunnel)))
|
||||
}
|
||||
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStateChange(state: State) {
|
||||
handleStateChange(TunnelState.from(state))
|
||||
}
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ class AmneziaStatistics(private val statistics: Statistics) : TunnelStatistics()
|
||||
PeerStats(
|
||||
rxBytes = stats.rxBytes,
|
||||
txBytes = stats.txBytes,
|
||||
latestHandshakeEpochMillis = stats.latestHandshakeEpochMillis
|
||||
latestHandshakeEpochMillis = stats.latestHandshakeEpochMillis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -2,17 +2,17 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
|
||||
|
||||
import org.amnezia.awg.crypto.Key
|
||||
|
||||
abstract class TunnelStatistics {
|
||||
abstract class TunnelStatistics {
|
||||
@JvmRecord
|
||||
data class PeerStats(val rxBytes: Long, val txBytes: Long, val latestHandshakeEpochMillis: Long)
|
||||
|
||||
abstract fun peerStats(peer: Key): PeerStats?
|
||||
|
||||
abstract fun isTunnelStale() : Boolean
|
||||
abstract fun isTunnelStale(): Boolean
|
||||
|
||||
abstract fun getPeers(): Array<Key>
|
||||
|
||||
abstract fun rx() : Long
|
||||
abstract fun rx(): Long
|
||||
|
||||
abstract fun tx() : Long
|
||||
abstract fun tx(): Long
|
||||
}
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
|
||||
import com.wireguard.android.backend.Statistics
|
||||
import org.amnezia.awg.crypto.Key
|
||||
|
||||
class WireGuardStatistics(private val statistics: Statistics) : TunnelStatistics() {
|
||||
class WireGuardStatistics(private val statistics: Statistics) : TunnelStatistics() {
|
||||
override fun peerStats(peer: Key): PeerStats? {
|
||||
val key = com.wireguard.crypto.Key.fromBase64(peer.toBase64())
|
||||
val peerStats = statistics.peer(key)
|
||||
@@ -11,7 +11,7 @@ class WireGuardStatistics(private val statistics: Statistics) : TunnelStatistics
|
||||
PeerStats(
|
||||
txBytes = peerStats.txBytes,
|
||||
rxBytes = peerStats.rxBytes,
|
||||
latestHandshakeEpochMillis = peerStats.latestHandshakeEpochMillis
|
||||
latestHandshakeEpochMillis = peerStats.latestHandshakeEpochMillis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,25 +4,16 @@ import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.zaneschepke.logcatter.Logcatter
|
||||
import com.zaneschepke.logcatter.model.LogMessage
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@@ -50,7 +41,7 @@ constructor() : ViewModel() {
|
||||
private fun requestPermissions() {
|
||||
_appUiState.update {
|
||||
it.copy(
|
||||
requestPermissions = true
|
||||
requestPermissions = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -58,12 +49,12 @@ constructor() : ViewModel() {
|
||||
fun permissionsRequested() {
|
||||
_appUiState.update {
|
||||
it.copy(
|
||||
requestPermissions = false
|
||||
requestPermissions = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun openWebPage(url: String, context : Context) {
|
||||
fun openWebPage(url: String, context: Context) {
|
||||
try {
|
||||
val webpage: Uri = Uri.parse(url)
|
||||
val intent = Intent(Intent.ACTION_VIEW, webpage).apply {
|
||||
@@ -79,7 +70,7 @@ constructor() : ViewModel() {
|
||||
fun onVpnPermissionAccepted() {
|
||||
_appUiState.update {
|
||||
it.copy(
|
||||
vpnPermissionAccepted = true
|
||||
vpnPermissionAccepted = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -122,33 +113,6 @@ constructor() : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
val logs = mutableStateListOf<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) {
|
||||
_appUiState.update {
|
||||
it.copy(
|
||||
|
||||
@@ -143,7 +143,7 @@ class MainActivity : AppCompatActivity() {
|
||||
if (!appUiState.vpnPermissionAccepted) {
|
||||
return@LaunchedEffect appViewModel.vpnIntent?.let {
|
||||
vpnActivityResultState.launch(
|
||||
it
|
||||
it,
|
||||
)
|
||||
}!!
|
||||
}
|
||||
@@ -155,7 +155,6 @@ class MainActivity : AppCompatActivity() {
|
||||
appViewModel.setNotificationPermissionAccepted(
|
||||
notificationPermissionState?.status?.isGranted ?: true,
|
||||
)
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) appViewModel.readLogCatOutput()
|
||||
}
|
||||
|
||||
LaunchedEffect(appUiState.snackbarMessageConsumed) {
|
||||
@@ -203,7 +202,9 @@ class MainActivity : AppCompatActivity() {
|
||||
NavHost(
|
||||
navController,
|
||||
startDestination =
|
||||
(if (PinManager.pinExists()) Screen.Lock.route else Screen.Main.route),
|
||||
//TODO disable pin lock
|
||||
//(if (PinManager.pinExists()) Screen.Lock.route else Screen.Main.route),
|
||||
Screen.Main.route,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(padding)
|
||||
@@ -237,30 +238,33 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
composable(Screen.Support.Logs.route) {
|
||||
LogsScreen(appViewModel)
|
||||
LogsScreen()
|
||||
}
|
||||
//TODO fix navigation for amnezia
|
||||
composable("${Screen.Config.route}/{id}?configType={configType}", arguments =
|
||||
listOf(
|
||||
navArgument("id") {
|
||||
type = NavType.StringType
|
||||
defaultValue = "0"
|
||||
},
|
||||
navArgument("configType") {
|
||||
type = NavType.StringType
|
||||
defaultValue = ConfigType.WIREGUARD.name
|
||||
}
|
||||
)
|
||||
composable(
|
||||
"${Screen.Config.route}/{id}?configType={configType}",
|
||||
arguments =
|
||||
listOf(
|
||||
navArgument("id") {
|
||||
type = NavType.StringType
|
||||
defaultValue = "0"
|
||||
},
|
||||
navArgument("configType") {
|
||||
type = NavType.StringType
|
||||
defaultValue = ConfigType.WIREGUARD.name
|
||||
},
|
||||
),
|
||||
) {
|
||||
val id = it.arguments?.getString("id")
|
||||
val configType = ConfigType.valueOf( it.arguments?.getString("configType") ?: ConfigType.WIREGUARD.name)
|
||||
val configType = ConfigType.valueOf(
|
||||
it.arguments?.getString("configType") ?: ConfigType.WIREGUARD.name,
|
||||
)
|
||||
if (!id.isNullOrBlank()) {
|
||||
ConfigScreen(
|
||||
navController = navController,
|
||||
tunnelId = id,
|
||||
appViewModel = appViewModel,
|
||||
focusRequester = focusRequester,
|
||||
configType = configType
|
||||
configType = configType,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
||||
@@ -52,9 +53,10 @@ fun RowListItem(
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(13 / 20f),
|
||||
) {
|
||||
icon()
|
||||
Text(text)
|
||||
Text(text, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
rowButton()
|
||||
}
|
||||
|
||||
@@ -44,19 +44,21 @@ fun SearchBar(onQuery: (queryString: String) -> Unit) {
|
||||
onQuery(onQueryChanged)
|
||||
},
|
||||
leadingIcon = {
|
||||
val icon = Icons.Rounded.Search
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Search,
|
||||
imageVector = icon,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
contentDescription = stringResource(id = R.string.search_icon),
|
||||
contentDescription = icon.name,
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
if (showClearIcon) {
|
||||
IconButton(onClick = { query = "" }) {
|
||||
val icon = Icons.Rounded.Clear
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Clear,
|
||||
imageVector = icon,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
contentDescription = stringResource(id = R.string.clear_icon),
|
||||
contentDescription = icon.name,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+9
-6
@@ -28,12 +28,15 @@ fun ConfigurationToggle(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(label, textAlign = TextAlign.Start, modifier = Modifier
|
||||
.weight(
|
||||
weight = 1.0f,
|
||||
fill = false,
|
||||
),
|
||||
softWrap = true)
|
||||
Text(
|
||||
label, textAlign = TextAlign.Start,
|
||||
modifier = Modifier
|
||||
.weight(
|
||||
weight = 1.0f,
|
||||
fill = false,
|
||||
),
|
||||
softWrap = true,
|
||||
)
|
||||
Switch(
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
|
||||
+31
-7
@@ -483,7 +483,7 @@ fun ConfigScreen(
|
||||
modifier = Modifier.width(IntrinsicSize.Min),
|
||||
)
|
||||
}
|
||||
if(configType == ConfigType.AMNEZIA) {
|
||||
if (configType == ConfigType.AMNEZIA) {
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.junkPacketCount,
|
||||
onValueChange = { value -> viewModel.onJunkPacketCountChanged(value) },
|
||||
@@ -496,7 +496,11 @@ fun ConfigScreen(
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.junkPacketMinSize,
|
||||
onValueChange = { value -> viewModel.onJunkPacketMinSizeChanged(value) },
|
||||
onValueChange = { value ->
|
||||
viewModel.onJunkPacketMinSizeChanged(
|
||||
value,
|
||||
)
|
||||
},
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.junk_packet_minimum_size),
|
||||
hint = stringResource(R.string.junk_packet_minimum_size).lowercase(),
|
||||
@@ -506,7 +510,11 @@ fun ConfigScreen(
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.junkPacketMaxSize,
|
||||
onValueChange = { value -> viewModel.onJunkPacketMaxSizeChanged(value) },
|
||||
onValueChange = { value ->
|
||||
viewModel.onJunkPacketMaxSizeChanged(
|
||||
value,
|
||||
)
|
||||
},
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.junk_packet_maximum_size),
|
||||
hint = stringResource(R.string.junk_packet_maximum_size).lowercase(),
|
||||
@@ -516,7 +524,11 @@ fun ConfigScreen(
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.initPacketJunkSize,
|
||||
onValueChange = { value -> viewModel.onInitPacketJunkSizeChanged(value) },
|
||||
onValueChange = { value ->
|
||||
viewModel.onInitPacketJunkSizeChanged(
|
||||
value,
|
||||
)
|
||||
},
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.init_packet_junk_size),
|
||||
hint = stringResource(R.string.init_packet_junk_size).lowercase(),
|
||||
@@ -546,7 +558,11 @@ fun ConfigScreen(
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.responsePacketMagicHeader,
|
||||
onValueChange = { value -> viewModel.onResponsePacketMagicHeader(value) },
|
||||
onValueChange = { value ->
|
||||
viewModel.onResponsePacketMagicHeader(
|
||||
value,
|
||||
)
|
||||
},
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.response_packet_magic_header),
|
||||
hint = stringResource(R.string.response_packet_magic_header).lowercase(),
|
||||
@@ -556,7 +572,11 @@ fun ConfigScreen(
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.underloadPacketMagicHeader,
|
||||
onValueChange = { value -> viewModel.onUnderloadPacketMagicHeader(value) },
|
||||
onValueChange = { value ->
|
||||
viewModel.onUnderloadPacketMagicHeader(
|
||||
value,
|
||||
)
|
||||
},
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.underload_packet_magic_header),
|
||||
hint = stringResource(R.string.underload_packet_magic_header).lowercase(),
|
||||
@@ -566,7 +586,11 @@ fun ConfigScreen(
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.transportPacketMagicHeader,
|
||||
onValueChange = { value -> viewModel.onTransportPacketMagicHeader(value) },
|
||||
onValueChange = { value ->
|
||||
viewModel.onTransportPacketMagicHeader(
|
||||
value,
|
||||
)
|
||||
},
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.transport_packet_magic_header),
|
||||
hint = stringResource(R.string.transport_packet_magic_header).lowercase(),
|
||||
|
||||
+3
-2
@@ -19,7 +19,7 @@ data class ConfigUiState(
|
||||
val isAmneziaEnabled: Boolean = false
|
||||
) {
|
||||
companion object {
|
||||
fun from(config : Config) : ConfigUiState {
|
||||
fun from(config: Config): ConfigUiState {
|
||||
val proxyPeers = config.peers.map { PeerProxy.from(it) }
|
||||
val proxyInterface = InterfaceProxy.from(config.`interface`)
|
||||
var include = true
|
||||
@@ -43,7 +43,8 @@ data class ConfigUiState(
|
||||
isAllApplicationsEnabled,
|
||||
)
|
||||
}
|
||||
fun from(config: org.amnezia.awg.config.Config) : ConfigUiState {
|
||||
|
||||
fun from(config: org.amnezia.awg.config.Config): ConfigUiState {
|
||||
//TODO update with new values
|
||||
val proxyPeers = config.peers.map { PeerProxy.from(it) }
|
||||
val proxyInterface = InterfaceProxy.from(config.`interface`)
|
||||
|
||||
+61
-39
@@ -16,6 +16,7 @@ import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
@@ -25,7 +26,7 @@ import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
|
||||
import com.zaneschepke.wireguardautotunnel.util.removeAt
|
||||
import com.zaneschepke.wireguardautotunnel.util.update
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
@@ -38,7 +39,8 @@ class ConfigViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val appDataRepository: AppDataRepository
|
||||
private val appDataRepository: AppDataRepository,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
||||
) : ViewModel() {
|
||||
|
||||
private val packageManager = WireGuardAutoTunnel.instance.packageManager
|
||||
@@ -47,7 +49,7 @@ constructor(
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
fun init(tunnelId: String) =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
val packages = getQueriedPackages("")
|
||||
val state =
|
||||
if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
||||
@@ -56,15 +58,16 @@ constructor(
|
||||
.firstOrNull { it.id.toString() == tunnelId }
|
||||
val isAmneziaEnabled = settingsRepository.getSettings().isAmneziaEnabled
|
||||
if (tunnelConfig != null) {
|
||||
(if(isAmneziaEnabled) {
|
||||
val amConfig = if(tunnelConfig.amQuick == "") tunnelConfig.wgQuick else tunnelConfig.amQuick
|
||||
(if (isAmneziaEnabled) {
|
||||
val amConfig =
|
||||
if (tunnelConfig.amQuick == "") tunnelConfig.wgQuick else tunnelConfig.amQuick
|
||||
ConfigUiState.from(TunnelConfig.configFromAmQuick(amConfig))
|
||||
} else ConfigUiState.from(TunnelConfig.configFromWgQuick(tunnelConfig.wgQuick))).copy(
|
||||
packages = packages,
|
||||
loading = false,
|
||||
tunnel = tunnelConfig,
|
||||
tunnelName = tunnelConfig.name,
|
||||
isAmneziaEnabled = isAmneziaEnabled
|
||||
isAmneziaEnabled = isAmneziaEnabled,
|
||||
)
|
||||
} else {
|
||||
ConfigUiState(loading = false, packages = packages)
|
||||
@@ -206,64 +209,82 @@ constructor(
|
||||
if (isAllApplicationsEnabled()) emptyCheckedPackagesList()
|
||||
if (_uiState.value.include) builder.includeApplications(_uiState.value.checkedPackageNames)
|
||||
if (!_uiState.value.include) builder.excludeApplications(_uiState.value.checkedPackageNames)
|
||||
if(_uiState.value.interfaceProxy.junkPacketCount.isNotEmpty()) {
|
||||
if (_uiState.value.interfaceProxy.junkPacketCount.isNotEmpty()) {
|
||||
builder.setJunkPacketCount(_uiState.value.interfaceProxy.junkPacketCount.trim().toInt())
|
||||
}
|
||||
if(_uiState.value.interfaceProxy.junkPacketMinSize.isNotEmpty()) {
|
||||
builder.setJunkPacketMinSize(_uiState.value.interfaceProxy.junkPacketMinSize.trim().toInt())
|
||||
if (_uiState.value.interfaceProxy.junkPacketMinSize.isNotEmpty()) {
|
||||
builder.setJunkPacketMinSize(
|
||||
_uiState.value.interfaceProxy.junkPacketMinSize.trim().toInt(),
|
||||
)
|
||||
}
|
||||
if(_uiState.value.interfaceProxy.junkPacketMaxSize.isNotEmpty()) {
|
||||
builder.setJunkPacketMaxSize(_uiState.value.interfaceProxy.junkPacketMaxSize.trim().toInt())
|
||||
if (_uiState.value.interfaceProxy.junkPacketMaxSize.isNotEmpty()) {
|
||||
builder.setJunkPacketMaxSize(
|
||||
_uiState.value.interfaceProxy.junkPacketMaxSize.trim().toInt(),
|
||||
)
|
||||
}
|
||||
if(_uiState.value.interfaceProxy.initPacketJunkSize.isNotEmpty()) {
|
||||
builder.setInitPacketJunkSize(_uiState.value.interfaceProxy.initPacketJunkSize.trim().toInt())
|
||||
if (_uiState.value.interfaceProxy.initPacketJunkSize.isNotEmpty()) {
|
||||
builder.setInitPacketJunkSize(
|
||||
_uiState.value.interfaceProxy.initPacketJunkSize.trim().toInt(),
|
||||
)
|
||||
}
|
||||
if(_uiState.value.interfaceProxy.responsePacketJunkSize.isNotEmpty()) {
|
||||
builder.setResponsePacketJunkSize(_uiState.value.interfaceProxy.responsePacketJunkSize.trim().toInt())
|
||||
if (_uiState.value.interfaceProxy.responsePacketJunkSize.isNotEmpty()) {
|
||||
builder.setResponsePacketJunkSize(
|
||||
_uiState.value.interfaceProxy.responsePacketJunkSize.trim().toInt(),
|
||||
)
|
||||
}
|
||||
if(_uiState.value.interfaceProxy.initPacketMagicHeader.isNotEmpty()) {
|
||||
builder.setInitPacketMagicHeader(_uiState.value.interfaceProxy.initPacketMagicHeader.trim().toLong())
|
||||
if (_uiState.value.interfaceProxy.initPacketMagicHeader.isNotEmpty()) {
|
||||
builder.setInitPacketMagicHeader(
|
||||
_uiState.value.interfaceProxy.initPacketMagicHeader.trim().toLong(),
|
||||
)
|
||||
}
|
||||
if(_uiState.value.interfaceProxy.responsePacketMagicHeader.isNotEmpty()) {
|
||||
builder.setResponsePacketMagicHeader(_uiState.value.interfaceProxy.responsePacketMagicHeader.trim().toLong())
|
||||
if (_uiState.value.interfaceProxy.responsePacketMagicHeader.isNotEmpty()) {
|
||||
builder.setResponsePacketMagicHeader(
|
||||
_uiState.value.interfaceProxy.responsePacketMagicHeader.trim().toLong(),
|
||||
)
|
||||
}
|
||||
if(_uiState.value.interfaceProxy.transportPacketMagicHeader.isNotEmpty()) {
|
||||
builder.setTransportPacketMagicHeader(_uiState.value.interfaceProxy.transportPacketMagicHeader.trim().toLong())
|
||||
if (_uiState.value.interfaceProxy.transportPacketMagicHeader.isNotEmpty()) {
|
||||
builder.setTransportPacketMagicHeader(
|
||||
_uiState.value.interfaceProxy.transportPacketMagicHeader.trim().toLong(),
|
||||
)
|
||||
}
|
||||
if(_uiState.value.interfaceProxy.underloadPacketMagicHeader.isNotEmpty()) {
|
||||
builder.setUnderloadPacketMagicHeader(_uiState.value.interfaceProxy.underloadPacketMagicHeader.trim().toLong())
|
||||
if (_uiState.value.interfaceProxy.underloadPacketMagicHeader.isNotEmpty()) {
|
||||
builder.setUnderloadPacketMagicHeader(
|
||||
_uiState.value.interfaceProxy.underloadPacketMagicHeader.trim().toLong(),
|
||||
)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private fun buildConfig() : Config {
|
||||
private fun buildConfig(): Config {
|
||||
val peerList = buildPeerListFromProxyPeers()
|
||||
val wgInterface = buildInterfaceListFromProxyInterface()
|
||||
return Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
||||
return Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
||||
}
|
||||
|
||||
private fun buildAmConfig() : org.amnezia.awg.config.Config {
|
||||
private fun buildAmConfig(): org.amnezia.awg.config.Config {
|
||||
val peerList = buildAmPeerListFromProxyPeers()
|
||||
val amInterface = buildAmInterfaceListFromProxyInterface()
|
||||
return org.amnezia.awg.config.Config.Builder().addPeers(peerList).setInterface(amInterface).build()
|
||||
return org.amnezia.awg.config.Config.Builder().addPeers(peerList).setInterface(amInterface)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun onSaveAllChanges(configType: ConfigType): Result<Unit> {
|
||||
return try {
|
||||
val wgQuick = buildConfig().toWgQuickString()
|
||||
val amQuick = if(configType == ConfigType.AMNEZIA) {
|
||||
val amQuick = if (configType == ConfigType.AMNEZIA) {
|
||||
buildAmConfig().toAwgQuickString()
|
||||
} else TunnelConfig.AM_QUICK_DEFAULT
|
||||
val tunnelConfig = when (uiState.value.tunnel) {
|
||||
null -> TunnelConfig(
|
||||
name = _uiState.value.tunnelName,
|
||||
wgQuick = wgQuick,
|
||||
amQuick = amQuick
|
||||
amQuick = amQuick,
|
||||
)
|
||||
|
||||
else -> uiState.value.tunnel!!.copy(
|
||||
name = _uiState.value.tunnelName,
|
||||
wgQuick = wgQuick,
|
||||
amQuick = amQuick
|
||||
amQuick = amQuick,
|
||||
)
|
||||
}
|
||||
updateTunnelConfig(tunnelConfig)
|
||||
@@ -430,14 +451,15 @@ constructor(
|
||||
fun onJunkPacketCountChanged(value: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketCount = value)
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketCount = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onJunkPacketMinSizeChanged(value: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketMinSize = value)
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketMinSize = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -445,7 +467,7 @@ constructor(
|
||||
fun onJunkPacketMaxSizeChanged(value: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketMaxSize = value)
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketMaxSize = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -453,7 +475,7 @@ constructor(
|
||||
fun onInitPacketJunkSizeChanged(value: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(initPacketJunkSize = value)
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(initPacketJunkSize = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -461,7 +483,7 @@ constructor(
|
||||
fun onResponsePacketJunkSize(value: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(responsePacketJunkSize = value)
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(responsePacketJunkSize = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -469,7 +491,7 @@ constructor(
|
||||
fun onInitPacketMagicHeader(value: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(initPacketMagicHeader = value)
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(initPacketMagicHeader = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -477,7 +499,7 @@ constructor(
|
||||
fun onResponsePacketMagicHeader(value: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(responsePacketMagicHeader = value)
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(responsePacketMagicHeader = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -485,7 +507,7 @@ constructor(
|
||||
fun onTransportPacketMagicHeader(value: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(transportPacketMagicHeader = value)
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(transportPacketMagicHeader = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -493,7 +515,7 @@ constructor(
|
||||
fun onUnderloadPacketMagicHeader(value: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(underloadPacketMagicHeader = value)
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(underloadPacketMagicHeader = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+20
-10
@@ -35,7 +35,8 @@ data class InterfaceProxy(
|
||||
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "",
|
||||
)
|
||||
}
|
||||
fun from(i: org.amnezia.awg.config.Interface) : InterfaceProxy {
|
||||
|
||||
fun from(i: org.amnezia.awg.config.Interface): InterfaceProxy {
|
||||
return InterfaceProxy(
|
||||
publicKey = i.keyPair.publicKey.toBase64().trim(),
|
||||
privateKey = i.keyPair.privateKey.toBase64().trim(),
|
||||
@@ -48,15 +49,24 @@ data class InterfaceProxy(
|
||||
""
|
||||
},
|
||||
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "",
|
||||
junkPacketCount = if(i.junkPacketCount.isPresent) i.junkPacketCount.get().toString() else "",
|
||||
junkPacketMinSize = if(i.junkPacketMinSize.isPresent) i.junkPacketMinSize.get().toString() else "",
|
||||
junkPacketMaxSize = if(i.junkPacketMaxSize.isPresent) i.junkPacketMaxSize.get().toString() else "",
|
||||
initPacketJunkSize = if(i.initPacketJunkSize.isPresent) i.initPacketJunkSize.get().toString() else "",
|
||||
responsePacketJunkSize = if(i.responsePacketJunkSize.isPresent) i.responsePacketJunkSize.get().toString() else "",
|
||||
initPacketMagicHeader = if(i.initPacketMagicHeader.isPresent) i.initPacketMagicHeader.get().toString() else "",
|
||||
responsePacketMagicHeader = if(i.responsePacketMagicHeader.isPresent) i.responsePacketMagicHeader.get().toString() else "",
|
||||
transportPacketMagicHeader = if(i.transportPacketMagicHeader.isPresent) i.transportPacketMagicHeader.get().toString() else "",
|
||||
underloadPacketMagicHeader = if(i.underloadPacketMagicHeader.isPresent) i.underloadPacketMagicHeader.get().toString() else "",
|
||||
junkPacketCount = if (i.junkPacketCount.isPresent) i.junkPacketCount.get()
|
||||
.toString() else "",
|
||||
junkPacketMinSize = if (i.junkPacketMinSize.isPresent) i.junkPacketMinSize.get()
|
||||
.toString() else "",
|
||||
junkPacketMaxSize = if (i.junkPacketMaxSize.isPresent) i.junkPacketMaxSize.get()
|
||||
.toString() else "",
|
||||
initPacketJunkSize = if (i.initPacketJunkSize.isPresent) i.initPacketJunkSize.get()
|
||||
.toString() else "",
|
||||
responsePacketJunkSize = if (i.responsePacketJunkSize.isPresent) i.responsePacketJunkSize.get()
|
||||
.toString() else "",
|
||||
initPacketMagicHeader = if (i.initPacketMagicHeader.isPresent) i.initPacketMagicHeader.get()
|
||||
.toString() else "",
|
||||
responsePacketMagicHeader = if (i.responsePacketMagicHeader.isPresent) i.responsePacketMagicHeader.get()
|
||||
.toString() else "",
|
||||
transportPacketMagicHeader = if (i.transportPacketMagicHeader.isPresent) i.transportPacketMagicHeader.get()
|
||||
.toString() else "",
|
||||
underloadPacketMagicHeader = if (i.underloadPacketMagicHeader.isPresent) i.underloadPacketMagicHeader.get()
|
||||
.toString() else "",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ data class PeerProxy(
|
||||
)
|
||||
}
|
||||
|
||||
fun from(peer: org.amnezia.awg.config.Peer) : PeerProxy {
|
||||
fun from(peer: org.amnezia.awg.config.Peer): PeerProxy {
|
||||
return PeerProxy(
|
||||
publicKey = peer.publicKey.toBase64(),
|
||||
preSharedKey =
|
||||
|
||||
+79
-53
@@ -14,6 +14,7 @@ import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.focusGroup
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
@@ -69,7 +70,6 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
@@ -111,8 +111,6 @@ import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.getMessage
|
||||
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
||||
import com.zaneschepke.wireguardautotunnel.util.truncateWithEllipsis
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -128,7 +126,7 @@ fun MainScreen(
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val context = LocalContext.current
|
||||
val isVisible = rememberSaveable { mutableStateOf(true) }
|
||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val sheetState = rememberModalBottomSheetState()
|
||||
var showBottomSheet by remember { mutableStateOf(false) }
|
||||
@@ -212,8 +210,8 @@ fun MainScreen(
|
||||
onResult = {
|
||||
if (it.contents != null) {
|
||||
scope.launch {
|
||||
viewModel.onTunnelQrResult(it.contents, configType).onFailure {
|
||||
appViewModel.showSnackbarMessage(it.getMessage(context))
|
||||
viewModel.onTunnelQrResult(it.contents, configType).onFailure { error ->
|
||||
appViewModel.showSnackbarMessage(error.getMessage(context))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,7 +244,9 @@ fun MainScreen(
|
||||
|
||||
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
|
||||
if (appViewModel.isRequiredPermissionGranted()) {
|
||||
if (checked) viewModel.onTunnelStart(tunnel, context) else viewModel.onTunnelStop(context)
|
||||
if (checked) viewModel.onTunnelStart(tunnel, context) else viewModel.onTunnelStop(
|
||||
context,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,7 +270,7 @@ fun MainScreen(
|
||||
Scaffold(
|
||||
modifier =
|
||||
Modifier.pointerInput(Unit) {
|
||||
if(uiState.tunnels.isNotEmpty()) {
|
||||
if (uiState.tunnels.isNotEmpty()) {
|
||||
detectTapGestures(
|
||||
onTap = {
|
||||
selectedTunnel = null
|
||||
@@ -285,31 +285,25 @@ fun MainScreen(
|
||||
visible = isVisible.value,
|
||||
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
||||
exit = slideOutVertically(targetOffsetY = { it * 2 }),
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.focusGroup(),
|
||||
) {
|
||||
val secondaryColor = MaterialTheme.colorScheme.secondary
|
||||
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||
var fobColor by remember { mutableStateOf(secondaryColor) }
|
||||
val tvFobColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||
val fobColor =
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) tvFobColor else secondaryColor
|
||||
val fobIconColor =
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) Color.White else MaterialTheme.colorScheme.background
|
||||
MultiFloatingActionButton(
|
||||
modifier =
|
||||
(if (
|
||||
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
||||
uiState.tunnels.isEmpty()
|
||||
)
|
||||
Modifier.focusRequester(focusRequester)
|
||||
else Modifier)
|
||||
.onFocusChanged {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||
}
|
||||
},
|
||||
fabIcon = FabIcon(
|
||||
iconRes = R.drawable.add,
|
||||
iconResAfterRotate = R.drawable.close,
|
||||
iconRotate = 180f
|
||||
iconRotate = 180f,
|
||||
),
|
||||
fabOption = FabOption(
|
||||
iconTint = MaterialTheme.colorScheme.background,
|
||||
backgroundTint = MaterialTheme.colorScheme.primary,
|
||||
iconTint = fobIconColor,
|
||||
backgroundTint = fobColor,
|
||||
),
|
||||
itemsMultiFab = listOf(
|
||||
MultiFabItem(
|
||||
@@ -318,24 +312,39 @@ fun MainScreen(
|
||||
stringResource(id = R.string.amnezia),
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(end = 10.dp)
|
||||
modifier = Modifier.padding(end = 10.dp),
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.size(40.dp),
|
||||
icon = R.drawable.add,
|
||||
value = ConfigType.AMNEZIA.name,
|
||||
miniFabOption = FabOption(
|
||||
backgroundTint = fobColor,
|
||||
fobIconColor,
|
||||
),
|
||||
),
|
||||
MultiFabItem(
|
||||
label = {
|
||||
Text(stringResource(id = R.string.wireguard), color = Color.White, textAlign = TextAlign.Center, modifier = Modifier.padding(end = 10.dp))
|
||||
Text(
|
||||
stringResource(id = R.string.wireguard),
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(end = 10.dp),
|
||||
)
|
||||
},
|
||||
icon = R.drawable.add,
|
||||
value = ConfigType.WIREGUARD.name
|
||||
value = ConfigType.WIREGUARD.name,
|
||||
miniFabOption = FabOption(
|
||||
backgroundTint = fobColor,
|
||||
fobIconColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
onFabItemClicked = {
|
||||
showBottomSheet = true
|
||||
configType = ConfigType.valueOf(it.value)
|
||||
},
|
||||
},
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
)
|
||||
}
|
||||
@@ -343,7 +352,10 @@ fun MainScreen(
|
||||
) {
|
||||
if (showBottomSheet) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { showBottomSheet = false },
|
||||
onDismissRequest = {
|
||||
showBottomSheet = false
|
||||
|
||||
},
|
||||
sheetState = sheetState,
|
||||
) {
|
||||
// Sheet content
|
||||
@@ -432,34 +444,48 @@ fun MainScreen(
|
||||
flingBehavior = ScrollableDefaults.flingBehavior(),
|
||||
) {
|
||||
item {
|
||||
val gettingStarted = buildAnnotatedString {
|
||||
append(stringResource(id = R.string.see_the))
|
||||
append(" ")
|
||||
pushStringAnnotation(tag = "gettingStarted", annotation = stringResource(id = R.string.getting_started_url))
|
||||
withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) {
|
||||
append(stringResource(id = R.string.getting_started_guide))
|
||||
}
|
||||
pop()
|
||||
append(" ")
|
||||
append(stringResource(R.string.unsure_how))
|
||||
append(".")
|
||||
}
|
||||
AnimatedVisibility(
|
||||
uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) {
|
||||
uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn(),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.padding(top = 100.dp)
|
||||
modifier = Modifier
|
||||
.padding(top = 100.dp)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
|
||||
ClickableText(
|
||||
modifier = Modifier.padding(vertical = 10.dp, horizontal = 24.dp),
|
||||
text = gettingStarted,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center),
|
||||
) {
|
||||
gettingStarted.getStringAnnotations(tag = "gettingStarted", it, it).firstOrNull()?.let { annotation ->
|
||||
appViewModel.openWebPage(annotation.item, context)
|
||||
val gettingStarted = buildAnnotatedString {
|
||||
append(stringResource(id = R.string.see_the))
|
||||
append(" ")
|
||||
pushStringAnnotation(
|
||||
tag = "gettingStarted",
|
||||
annotation = stringResource(id = R.string.getting_started_url),
|
||||
)
|
||||
withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) {
|
||||
append(stringResource(id = R.string.getting_started_guide))
|
||||
}
|
||||
pop()
|
||||
append(" ")
|
||||
append(stringResource(R.string.unsure_how))
|
||||
append(".")
|
||||
}
|
||||
Text(
|
||||
text = stringResource(R.string.no_tunnels),
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
ClickableText(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 10.dp, horizontal = 24.dp),
|
||||
text = gettingStarted,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
) {
|
||||
gettingStarted.getStringAnnotations(tag = "gettingStarted", it, it)
|
||||
.firstOrNull()?.let { annotation ->
|
||||
appViewModel.openWebPage(annotation.item, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -562,7 +588,7 @@ fun MainScreen(
|
||||
.size(if (icon == circleIcon) 15.dp else 20.dp),
|
||||
)
|
||||
},
|
||||
text = tunnel.name.truncateWithEllipsis(Constants.ALLOWED_DISPLAY_NAME_LENGTH),
|
||||
text = tunnel.name,
|
||||
onHold = {
|
||||
if (
|
||||
(uiState.vpnState.status == TunnelState.UP) &&
|
||||
|
||||
+154
-96
@@ -11,6 +11,7 @@ import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
@@ -18,7 +19,7 @@ import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
|
||||
import com.zaneschepke.wireguardautotunnel.util.toWgQuickString
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
@@ -35,7 +36,8 @@ class MainViewModel
|
||||
constructor(
|
||||
private val appDataRepository: AppDataRepository,
|
||||
private val serviceManager: ServiceManager,
|
||||
val vpnService: VpnService
|
||||
val vpnService: VpnService,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
||||
) : ViewModel() {
|
||||
|
||||
val uiState =
|
||||
@@ -52,13 +54,12 @@ constructor(
|
||||
MainUiState(),
|
||||
)
|
||||
|
||||
private fun stopWatcherService(context: Context) =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
serviceManager.stopWatcherService(context)
|
||||
}
|
||||
private fun stopWatcherService(context: Context) {
|
||||
serviceManager.stopWatcherService(context)
|
||||
}
|
||||
|
||||
fun onDelete(tunnel: TunnelConfig, context: Context) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
viewModelScope.launch {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
val isPrimary = tunnel.isPrimaryTunnel
|
||||
if (appDataRepository.tunnels.count() == 1 || isPrimary) {
|
||||
@@ -80,7 +81,7 @@ constructor(
|
||||
}
|
||||
|
||||
fun onTunnelStart(tunnelConfig: TunnelConfig, context: Context) =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
viewModelScope.launch {
|
||||
Timber.d("On start called!")
|
||||
serviceManager.startVpnService(
|
||||
context,
|
||||
@@ -91,41 +92,42 @@ constructor(
|
||||
|
||||
|
||||
fun onTunnelStop(context: Context) =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
viewModelScope.launch {
|
||||
Timber.i("Stopping active tunnel")
|
||||
serviceManager.stopVpnService(context, isManualStop = true)
|
||||
}
|
||||
|
||||
private fun validateConfigString(config: String, configType: ConfigType) {
|
||||
when(configType) {
|
||||
when (configType) {
|
||||
ConfigType.AMNEZIA -> TunnelConfig.configFromAmQuick(config)
|
||||
ConfigType.WIREGUARD -> TunnelConfig.configFromWgQuick(config)
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateQrCodeDefaultName(config : String, configType: ConfigType) : String {
|
||||
private fun generateQrCodeDefaultName(config: String, configType: ConfigType): String {
|
||||
return try {
|
||||
when(configType) {
|
||||
when (configType) {
|
||||
ConfigType.AMNEZIA -> {
|
||||
TunnelConfig.configFromAmQuick(config).peers[0].endpoint.get().host
|
||||
}
|
||||
|
||||
ConfigType.WIREGUARD -> {
|
||||
TunnelConfig.configFromWgQuick(config).peers[0].endpoint.get().host
|
||||
}
|
||||
}
|
||||
} catch (e : Exception) {
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
NumberUtils.generateRandomTunnelName()
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateQrCodeTunnelName(config : String, configType: ConfigType) : String {
|
||||
private fun generateQrCodeTunnelName(config: String, configType: ConfigType): String {
|
||||
var defaultName = generateQrCodeDefaultName(config, configType)
|
||||
val lines = config.lines().toMutableList()
|
||||
val linesIterator = lines.iterator()
|
||||
while(linesIterator.hasNext()) {
|
||||
while (linesIterator.hasNext()) {
|
||||
val next = linesIterator.next()
|
||||
if(next.contains(Constants.QR_CODE_NAME_PROPERTY)) {
|
||||
if (next.contains(Constants.QR_CODE_NAME_PROPERTY)) {
|
||||
defaultName = next.substringAfter(Constants.QR_CODE_NAME_PROPERTY).trim()
|
||||
break
|
||||
}
|
||||
@@ -134,121 +136,177 @@ constructor(
|
||||
}
|
||||
|
||||
suspend fun onTunnelQrResult(result: String, configType: ConfigType): Result<Unit> {
|
||||
return try {
|
||||
validateConfigString(result, configType)
|
||||
val tunnelName = makeTunnelNameUnique(generateQrCodeTunnelName(result, configType))
|
||||
val tunnelConfig = when(configType) {
|
||||
ConfigType.AMNEZIA ->{
|
||||
TunnelConfig(name = tunnelName, amQuick = result,
|
||||
wgQuick = TunnelConfig.configFromAmQuick(result).toWgQuickString())
|
||||
return withContext(ioDispatcher) {
|
||||
try {
|
||||
validateConfigString(result, configType)
|
||||
val tunnelName = makeTunnelNameUnique(generateQrCodeTunnelName(result, configType))
|
||||
val tunnelConfig = when (configType) {
|
||||
ConfigType.AMNEZIA -> {
|
||||
TunnelConfig(
|
||||
name = tunnelName, amQuick = result,
|
||||
wgQuick = TunnelConfig.configFromAmQuick(result).toWgQuickString(),
|
||||
)
|
||||
}
|
||||
|
||||
ConfigType.WIREGUARD -> TunnelConfig(name = tunnelName, wgQuick = result)
|
||||
}
|
||||
ConfigType.WIREGUARD -> TunnelConfig(name = tunnelName, wgQuick = result)
|
||||
addTunnel(tunnelConfig)
|
||||
Result.success(Unit)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
Result.failure(WgTunnelExceptions.InvalidQrCode())
|
||||
}
|
||||
addTunnel(tunnelConfig)
|
||||
Result.success(Unit)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
Result.failure(WgTunnelExceptions.InvalidQrCode())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun makeTunnelNameUnique(name : String) : String {
|
||||
val tunnels = appDataRepository.tunnels.getAll()
|
||||
var tunnelName = name
|
||||
var num = 1
|
||||
while (tunnels.any { it.name == tunnelName }) {
|
||||
tunnelName = name + "(${num})"
|
||||
num++
|
||||
private suspend fun makeTunnelNameUnique(name: String): String {
|
||||
return withContext(ioDispatcher) {
|
||||
val tunnels = appDataRepository.tunnels.getAll()
|
||||
var tunnelName = name
|
||||
var num = 1
|
||||
while (tunnels.any { it.name == tunnelName }) {
|
||||
tunnelName = name + "(${num})"
|
||||
num++
|
||||
}
|
||||
tunnelName
|
||||
}
|
||||
return tunnelName
|
||||
}
|
||||
|
||||
private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String, type: ConfigType) {
|
||||
var amQuick : String? = null
|
||||
private fun saveTunnelConfigFromStream(
|
||||
stream: InputStream,
|
||||
fileName: String,
|
||||
type: ConfigType
|
||||
) {
|
||||
var amQuick: String? = null
|
||||
val wgQuick = stream.use {
|
||||
when(type) {
|
||||
when (type) {
|
||||
ConfigType.AMNEZIA -> {
|
||||
val config = org.amnezia.awg.config.Config.parse(it)
|
||||
amQuick = config.toAwgQuickString()
|
||||
config.toWgQuickString()
|
||||
}
|
||||
|
||||
ConfigType.WIREGUARD -> {
|
||||
Config.parse(it).toWgQuickString()
|
||||
}
|
||||
}
|
||||
}
|
||||
val tunnelName = makeTunnelNameUnique(getNameFromFileName(fileName))
|
||||
addTunnel(TunnelConfig(name = tunnelName, wgQuick = wgQuick, amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT))
|
||||
viewModelScope.launch {
|
||||
val tunnelName = makeTunnelNameUnique(getNameFromFileName(fileName))
|
||||
addTunnel(
|
||||
TunnelConfig(
|
||||
name = tunnelName,
|
||||
wgQuick = wgQuick,
|
||||
amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInputStreamFromUri(uri: Uri, context: Context): InputStream? {
|
||||
return context.applicationContext.contentResolver.openInputStream(uri)
|
||||
}
|
||||
|
||||
suspend fun onTunnelFileSelected(uri: Uri, configType: ConfigType, context: Context): Result<Unit> {
|
||||
return try {
|
||||
if (isValidUriContentScheme(uri)) {
|
||||
val fileName = getFileName(context, uri)
|
||||
return when (getFileExtensionFromFileName(fileName)) {
|
||||
Constants.CONF_FILE_EXTENSION ->
|
||||
saveTunnelFromConfUri(fileName, uri, configType, context)
|
||||
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri, configType, context)
|
||||
else -> Result.failure(WgTunnelExceptions.InvalidFileExtension())
|
||||
suspend fun onTunnelFileSelected(
|
||||
uri: Uri,
|
||||
configType: ConfigType,
|
||||
context: Context
|
||||
): Result<Unit> {
|
||||
return withContext(ioDispatcher) {
|
||||
try {
|
||||
if (isValidUriContentScheme(uri)) {
|
||||
val fileName = getFileName(context, uri)
|
||||
return@withContext when (getFileExtensionFromFileName(fileName)) {
|
||||
Constants.CONF_FILE_EXTENSION ->
|
||||
saveTunnelFromConfUri(fileName, uri, configType, context)
|
||||
|
||||
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(
|
||||
uri,
|
||||
configType,
|
||||
context,
|
||||
)
|
||||
|
||||
else -> Result.failure(WgTunnelExceptions.InvalidFileExtension())
|
||||
}
|
||||
} else {
|
||||
Result.failure(WgTunnelExceptions.InvalidFileExtension())
|
||||
}
|
||||
} else {
|
||||
Result.failure(WgTunnelExceptions.InvalidFileExtension())
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
Result.failure(WgTunnelExceptions.FileReadFailed())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
Result.failure(WgTunnelExceptions.FileReadFailed())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveTunnelsFromZipUri(uri: Uri, configType: ConfigType, context: Context) : Result<Unit> {
|
||||
return ZipInputStream(getInputStreamFromUri(uri, context)).use { zip ->
|
||||
generateSequence { zip.nextEntry }
|
||||
.filterNot {
|
||||
it.isDirectory ||
|
||||
getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
|
||||
}
|
||||
.forEach {
|
||||
val name = getNameFromFileName(it.name)
|
||||
withContext(viewModelScope.coroutineContext + Dispatchers.IO) {
|
||||
try {
|
||||
var amQuick : String? = null
|
||||
val wgQuick =
|
||||
when(configType) {
|
||||
ConfigType.AMNEZIA -> {
|
||||
val config = org.amnezia.awg.config.Config.parse(zip)
|
||||
amQuick = config.toAwgQuickString()
|
||||
config.toWgQuickString()
|
||||
private suspend fun saveTunnelsFromZipUri(
|
||||
uri: Uri,
|
||||
configType: ConfigType,
|
||||
context: Context
|
||||
): Result<Unit> {
|
||||
return withContext(ioDispatcher) {
|
||||
ZipInputStream(getInputStreamFromUri(uri, context)).use { zip ->
|
||||
generateSequence { zip.nextEntry }
|
||||
.filterNot {
|
||||
it.isDirectory ||
|
||||
getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
|
||||
}
|
||||
.forEach {
|
||||
val name = getNameFromFileName(it.name)
|
||||
withContext(viewModelScope.coroutineContext) {
|
||||
try {
|
||||
var amQuick: String? = null
|
||||
val wgQuick =
|
||||
when (configType) {
|
||||
ConfigType.AMNEZIA -> {
|
||||
val config = org.amnezia.awg.config.Config.parse(zip)
|
||||
amQuick = config.toAwgQuickString()
|
||||
config.toWgQuickString()
|
||||
}
|
||||
|
||||
ConfigType.WIREGUARD -> {
|
||||
Config.parse(zip).toWgQuickString()
|
||||
}
|
||||
}
|
||||
ConfigType.WIREGUARD -> {
|
||||
Config.parse(zip).toWgQuickString()
|
||||
}
|
||||
}
|
||||
addTunnel(TunnelConfig(name = makeTunnelNameUnique(name), wgQuick = wgQuick, amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT))
|
||||
Result.success(Unit)
|
||||
} catch (e : Exception) {
|
||||
Result.failure(WgTunnelExceptions.FileReadFailed())
|
||||
addTunnel(
|
||||
TunnelConfig(
|
||||
name = makeTunnelNameUnique(name),
|
||||
wgQuick = wgQuick,
|
||||
amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT,
|
||||
),
|
||||
)
|
||||
Result.success(Unit)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(WgTunnelExceptions.FileReadFailed())
|
||||
}
|
||||
}
|
||||
}
|
||||
Result.success(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveTunnelFromConfUri(
|
||||
name: String,
|
||||
uri: Uri,
|
||||
configType: ConfigType,
|
||||
context: Context
|
||||
): Result<Unit> {
|
||||
return withContext(ioDispatcher) {
|
||||
val stream = getInputStreamFromUri(uri, context)
|
||||
return@withContext if (stream != null) {
|
||||
try {
|
||||
saveTunnelConfigFromStream(stream, name, configType)
|
||||
} catch (e: Exception) {
|
||||
return@withContext Result.failure(WgTunnelExceptions.ConfigParseError())
|
||||
}
|
||||
Result.success(Unit)
|
||||
Result.success(Unit)
|
||||
} else {
|
||||
Result.failure(WgTunnelExceptions.FileReadFailed())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveTunnelFromConfUri(name: String, uri: Uri, configType: ConfigType, context: Context): Result<Unit> {
|
||||
val stream = getInputStreamFromUri(uri, context)
|
||||
return if (stream != null) {
|
||||
saveTunnelConfigFromStream(stream, name, configType)
|
||||
Result.success(Unit)
|
||||
} else {
|
||||
Result.failure(WgTunnelExceptions.FileReadFailed())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
||||
private fun addTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
|
||||
val firstTunnel = appDataRepository.tunnels.count() == 0
|
||||
saveTunnel(tunnelConfig)
|
||||
if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
||||
@@ -266,7 +324,7 @@ constructor(
|
||||
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
|
||||
}
|
||||
|
||||
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
|
||||
private fun saveTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
|
||||
appDataRepository.tunnels.save(tunnelConfig)
|
||||
}
|
||||
|
||||
@@ -317,7 +375,7 @@ constructor(
|
||||
}
|
||||
|
||||
private fun saveSettings(settings: Settings) =
|
||||
viewModelScope.launch(Dispatchers.IO) { appDataRepository.settings.save(settings) }
|
||||
viewModelScope.launch { appDataRepository.settings.save(settings) }
|
||||
|
||||
|
||||
fun onCopyTunnel(tunnel: TunnelConfig?) = viewModelScope.launch {
|
||||
|
||||
+73
-53
@@ -1,7 +1,11 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.options
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.focusGroup
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -12,6 +16,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
@@ -39,7 +44,6 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
@@ -114,59 +118,75 @@ fun OptionsScreen(
|
||||
Scaffold(
|
||||
floatingActionButton = {
|
||||
val secondaryColor = MaterialTheme.colorScheme.secondary
|
||||
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||
var fobColor by remember { mutableStateOf(secondaryColor) }
|
||||
MultiFloatingActionButton(
|
||||
modifier =
|
||||
(if (
|
||||
WireGuardAutoTunnel.isRunningOnAndroidTv()
|
||||
)
|
||||
Modifier.focusRequester(focusRequester)
|
||||
else Modifier)
|
||||
.onFocusChanged {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||
}
|
||||
val tvFobColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||
val fobColor =
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) tvFobColor else secondaryColor
|
||||
val fobIconColor =
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) Color.White else MaterialTheme.colorScheme.background
|
||||
AnimatedVisibility(
|
||||
visible = true,
|
||||
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
||||
exit = slideOutVertically(targetOffsetY = { it * 2 }),
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.focusGroup(),
|
||||
) {
|
||||
MultiFloatingActionButton(
|
||||
fabIcon = FabIcon(
|
||||
iconRes = R.drawable.edit,
|
||||
iconResAfterRotate = R.drawable.close,
|
||||
iconRotate = 180f,
|
||||
),
|
||||
fabOption = FabOption(
|
||||
iconTint = fobIconColor,
|
||||
backgroundTint = fobColor,
|
||||
),
|
||||
itemsMultiFab = listOf(
|
||||
MultiFabItem(
|
||||
label = {
|
||||
Text(
|
||||
stringResource(id = R.string.amnezia),
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(end = 10.dp),
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.size(40.dp),
|
||||
icon = R.drawable.edit,
|
||||
value = ConfigType.AMNEZIA.name,
|
||||
miniFabOption = FabOption(
|
||||
backgroundTint = fobColor,
|
||||
fobIconColor,
|
||||
),
|
||||
),
|
||||
MultiFabItem(
|
||||
label = {
|
||||
Text(
|
||||
stringResource(id = R.string.wireguard),
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(end = 10.dp),
|
||||
)
|
||||
},
|
||||
icon = R.drawable.edit,
|
||||
value = ConfigType.WIREGUARD.name,
|
||||
miniFabOption = FabOption(
|
||||
backgroundTint = fobColor,
|
||||
fobIconColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
onFabItemClicked = {
|
||||
val configType = ConfigType.valueOf(it.value)
|
||||
navController.navigate(
|
||||
"${Screen.Config.route}/${tunnelId}?configType=${configType.name}",
|
||||
)
|
||||
},
|
||||
fabIcon = FabIcon(
|
||||
iconRes = R.drawable.edit,
|
||||
iconResAfterRotate = R.drawable.close,
|
||||
iconRotate = 180f
|
||||
),
|
||||
fabOption = FabOption(
|
||||
iconTint = MaterialTheme.colorScheme.background,
|
||||
backgroundTint = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
itemsMultiFab = listOf(
|
||||
MultiFabItem(
|
||||
label = {
|
||||
Text(
|
||||
stringResource(id = R.string.amnezia),
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(end = 10.dp)
|
||||
)
|
||||
},
|
||||
icon = R.drawable.edit,
|
||||
value = ConfigType.AMNEZIA.name,
|
||||
),
|
||||
MultiFabItem(
|
||||
label = {
|
||||
Text(stringResource(id = R.string.wireguard), color = Color.White, textAlign = TextAlign.Center, modifier = Modifier.padding(end = 10.dp))
|
||||
},
|
||||
icon = R.drawable.edit,
|
||||
value = ConfigType.WIREGUARD.name
|
||||
),
|
||||
),
|
||||
onFabItemClicked = {
|
||||
val configType = ConfigType.valueOf(it.value)
|
||||
navController.navigate(
|
||||
"${Screen.Config.route}/${tunnelId}?configType=${configType.name}",
|
||||
)
|
||||
},
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
)
|
||||
}
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
|
||||
+5
-6
@@ -9,7 +9,6 @@ import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@@ -45,12 +44,12 @@ constructor(
|
||||
fun init(tunnelId: String) {
|
||||
_optionState.update {
|
||||
it.copy(
|
||||
id = tunnelId
|
||||
id = tunnelId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onDeleteRunSSID(ssid: String) = viewModelScope.launch(Dispatchers.IO) {
|
||||
fun onDeleteRunSSID(ssid: String) = viewModelScope.launch {
|
||||
uiState.value.tunnel?.let {
|
||||
appDataRepository.tunnels.save(
|
||||
tunnelConfig = it.copy(
|
||||
@@ -60,7 +59,7 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveTunnel(tunnelConfig: TunnelConfig?) = viewModelScope.launch(Dispatchers.IO) {
|
||||
private fun saveTunnel(tunnelConfig: TunnelConfig?) = viewModelScope.launch {
|
||||
tunnelConfig?.let {
|
||||
appDataRepository.tunnels.save(it)
|
||||
}
|
||||
@@ -81,7 +80,7 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun onToggleIsMobileDataTunnel() = viewModelScope.launch(Dispatchers.IO) {
|
||||
fun onToggleIsMobileDataTunnel() = viewModelScope.launch {
|
||||
uiState.value.tunnel?.let {
|
||||
if (it.isMobileDataTunnel) {
|
||||
appDataRepository.tunnels.updateMobileDataTunnel(null)
|
||||
@@ -89,7 +88,7 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun onTogglePrimaryTunnel() = viewModelScope.launch(Dispatchers.IO) {
|
||||
fun onTogglePrimaryTunnel() = viewModelScope.launch {
|
||||
if (uiState.value.tunnel != null) {
|
||||
appDataRepository.tunnels.updatePrimaryTunnel(
|
||||
when (uiState.value.isDefaultTunnel) {
|
||||
|
||||
+46
-38
@@ -44,6 +44,7 @@ import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -70,7 +71,6 @@ import androidx.navigation.NavController
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
@@ -81,9 +81,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.getMessage
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
@@ -100,14 +98,15 @@ fun SettingsScreen(
|
||||
navController: NavController,
|
||||
focusRequester: FocusRequester,
|
||||
) {
|
||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||
val context = LocalContext.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollState = rememberScrollState()
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val pinExists = remember { mutableStateOf(PinManager.pinExists()) }
|
||||
//val pinExists = remember { mutableStateOf(PinManager.pinExists()) }
|
||||
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val kernelSupport by viewModel.kernelSupport.collectAsStateWithLifecycle()
|
||||
|
||||
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
var currentText by remember { mutableStateOf("") }
|
||||
@@ -119,6 +118,10 @@ fun SettingsScreen(
|
||||
val screenPadding = 5.dp
|
||||
val fillMaxWidth = .85f
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.checkKernelSupport()
|
||||
}
|
||||
|
||||
val startForResult =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
@@ -137,18 +140,22 @@ fun SettingsScreen(
|
||||
}
|
||||
file
|
||||
}
|
||||
val amFiles = uiState.tunnels.mapNotNull { config -> if(config.amQuick != TunnelConfig.AM_QUICK_DEFAULT) {
|
||||
val file = File(context.cacheDir, "${config.name}-am.conf")
|
||||
file.outputStream().use {
|
||||
it.write(config.amQuick.toByteArray())
|
||||
val amFiles = uiState.tunnels.mapNotNull { config ->
|
||||
if (config.amQuick != TunnelConfig.AM_QUICK_DEFAULT) {
|
||||
val file = File(context.cacheDir, "${config.name}-am.conf")
|
||||
file.outputStream().use {
|
||||
it.write(config.amQuick.toByteArray())
|
||||
}
|
||||
file
|
||||
} else null
|
||||
}
|
||||
scope.launch {
|
||||
viewModel.onExportTunnels(wgFiles + amFiles).onFailure {
|
||||
appViewModel.showSnackbarMessage(it.getMessage(context))
|
||||
}.onSuccess {
|
||||
didExportFiles = true
|
||||
appViewModel.showSnackbarMessage(context.getString(R.string.exported_configs_message))
|
||||
}
|
||||
file
|
||||
} else null }
|
||||
FileUtils.saveFilesToZip(context, wgFiles + amFiles).onFailure {
|
||||
appViewModel.showSnackbarMessage(it.getMessage(context))
|
||||
}.onSuccess {
|
||||
didExportFiles = true
|
||||
appViewModel.showSnackbarMessage(context.getString(R.string.exported_configs_message))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
@@ -190,11 +197,9 @@ fun SettingsScreen(
|
||||
}
|
||||
|
||||
fun openSettings() {
|
||||
scope.launch {
|
||||
val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
intentSettings.data = Uri.fromParts("package", context.packageName, null)
|
||||
context.startActivity(intentSettings)
|
||||
}
|
||||
val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
intentSettings.data = Uri.fromParts("package", context.packageName, null)
|
||||
context.startActivity(intentSettings)
|
||||
}
|
||||
|
||||
fun checkFineLocationGranted() {
|
||||
@@ -591,7 +596,7 @@ fun SettingsScreen(
|
||||
viewModel.onToggleAmnezia()
|
||||
},
|
||||
)
|
||||
if (WgQuickBackend.hasKernelSupport()) {
|
||||
if (kernelSupport) {
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.use_kernel),
|
||||
enabled =
|
||||
@@ -601,8 +606,10 @@ fun SettingsScreen(
|
||||
checked = uiState.settings.isKernelEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = {
|
||||
viewModel.onToggleKernelMode().onFailure {
|
||||
appViewModel.showSnackbarMessage(it.getMessage(context))
|
||||
scope.launch {
|
||||
viewModel.onToggleKernelMode().onFailure {
|
||||
appViewModel.showSnackbarMessage(it.getMessage(context))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -645,20 +652,21 @@ fun SettingsScreen(
|
||||
onCheckChanged = { viewModel.onToggleShortcutsEnabled() },
|
||||
)
|
||||
}
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.enable_app_lock),
|
||||
enabled = true,
|
||||
checked = pinExists.value,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = {
|
||||
if (pinExists.value) {
|
||||
PinManager.clearPin()
|
||||
pinExists.value = PinManager.pinExists()
|
||||
} else {
|
||||
navController.navigate(Screen.Lock.route)
|
||||
}
|
||||
},
|
||||
)
|
||||
// TODO disable for now
|
||||
// ConfigurationToggle(
|
||||
// stringResource(R.string.enable_app_lock),
|
||||
// enabled = true,
|
||||
// checked = pinExists.value,
|
||||
// padding = screenPadding,
|
||||
// onCheckChanged = {
|
||||
// if (pinExists.value) {
|
||||
// PinManager.clearPin()
|
||||
// pinExists.value = PinManager.pinExists()
|
||||
// } else {
|
||||
// navController.navigate(Screen.Lock.route)
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
||||
+1
-1
@@ -9,5 +9,5 @@ data class SettingsUiState(
|
||||
val tunnels: List<TunnelConfig> = emptyList(),
|
||||
val vpnState: VpnState = VpnState(),
|
||||
val isLocationDisclosureShown: Boolean = true,
|
||||
val isBatteryOptimizeDisableShown: Boolean = false
|
||||
val isBatteryOptimizeDisableShown: Boolean = false,
|
||||
)
|
||||
|
||||
+51
-21
@@ -5,21 +5,31 @@ import android.location.LocationManager
|
||||
import androidx.core.location.LocationManagerCompat
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.wireguard.android.util.RootShell
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
@HiltViewModel
|
||||
class SettingsViewModel
|
||||
@@ -27,10 +37,15 @@ class SettingsViewModel
|
||||
constructor(
|
||||
private val appDataRepository: AppDataRepository,
|
||||
private val serviceManager: ServiceManager,
|
||||
private val rootShell: RootShell,
|
||||
private val rootShell: Provider<RootShell>,
|
||||
private val fileUtils: FileUtils,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
vpnService: VpnService
|
||||
) : ViewModel() {
|
||||
|
||||
private val _kernelSupport = MutableStateFlow(false)
|
||||
val kernelSupport = _kernelSupport.asStateFlow()
|
||||
|
||||
val uiState =
|
||||
combine(
|
||||
appDataRepository.settings.getSettingsFlow(),
|
||||
@@ -90,6 +105,10 @@ constructor(
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun onExportTunnels(files: List<File>): Result<Unit> {
|
||||
return fileUtils.saveFilesToZip(files)
|
||||
}
|
||||
|
||||
fun onToggleAutoTunnel(context: Context) =
|
||||
viewModelScope.launch {
|
||||
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
|
||||
@@ -160,7 +179,7 @@ constructor(
|
||||
}
|
||||
|
||||
fun onToggleAmnezia() = viewModelScope.launch {
|
||||
if(uiState.value.settings.isKernelEnabled) {
|
||||
if (uiState.value.settings.isKernelEnabled) {
|
||||
saveKernelMode(false)
|
||||
}
|
||||
saveAmneziaMode(!uiState.value.settings.isAmneziaEnabled)
|
||||
@@ -169,32 +188,34 @@ constructor(
|
||||
private fun saveAmneziaMode(on: Boolean) {
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
isAmneziaEnabled = on
|
||||
)
|
||||
isAmneziaEnabled = on,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onToggleKernelMode(): Result<Unit> {
|
||||
if (!uiState.value.settings.isKernelEnabled) {
|
||||
try {
|
||||
rootShell.start()
|
||||
Timber.i("Root shell accepted!")
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
isKernelEnabled = true,
|
||||
isAmneziaEnabled = false,
|
||||
),
|
||||
)
|
||||
suspend fun onToggleKernelMode(): Result<Unit> {
|
||||
return withContext(ioDispatcher) {
|
||||
if (!uiState.value.settings.isKernelEnabled) {
|
||||
try {
|
||||
rootShell.get().start()
|
||||
Timber.i("Root shell accepted!")
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
isKernelEnabled = true,
|
||||
isAmneziaEnabled = false,
|
||||
),
|
||||
)
|
||||
|
||||
} catch (e: RootShell.RootShellException) {
|
||||
Timber.e(e)
|
||||
} catch (e: RootShell.RootShellException) {
|
||||
Timber.e(e)
|
||||
saveKernelMode(on = false)
|
||||
return@withContext Result.failure(WgTunnelExceptions.RootDenied())
|
||||
}
|
||||
} else {
|
||||
saveKernelMode(on = false)
|
||||
return Result.failure(WgTunnelExceptions.RootDenied())
|
||||
}
|
||||
} else {
|
||||
saveKernelMode(on = false)
|
||||
Result.success(Unit)
|
||||
}
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
fun onToggleRestartOnPing() = viewModelScope.launch {
|
||||
@@ -204,4 +225,13 @@ constructor(
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun checkKernelSupport() = viewModelScope.launch {
|
||||
val kernelSupport = withContext(ioDispatcher) {
|
||||
WgQuickBackend.hasKernelSupport()
|
||||
}
|
||||
_kernelSupport.update {
|
||||
kernelSupport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
-6
@@ -107,7 +107,12 @@ fun SupportScreen(
|
||||
modifier = Modifier.padding(bottom = 20.dp),
|
||||
)
|
||||
TextButton(
|
||||
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.docs_url), context) },
|
||||
onClick = {
|
||||
appViewModel.openWebPage(
|
||||
context.resources.getString(R.string.docs_url),
|
||||
context,
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(vertical = 5.dp)
|
||||
.focusRequester(focusRequester),
|
||||
@@ -129,7 +134,7 @@ fun SupportScreen(
|
||||
weight = 1.0f,
|
||||
fill = false,
|
||||
),
|
||||
softWrap = true
|
||||
softWrap = true,
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
@@ -143,7 +148,12 @@ fun SupportScreen(
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
TextButton(
|
||||
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.telegram_url), context) },
|
||||
onClick = {
|
||||
appViewModel.openWebPage(
|
||||
context.resources.getString(R.string.telegram_url),
|
||||
context,
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(vertical = 5.dp),
|
||||
) {
|
||||
Row(
|
||||
@@ -175,7 +185,12 @@ fun SupportScreen(
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
TextButton(
|
||||
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.github_url), context) },
|
||||
onClick = {
|
||||
appViewModel.openWebPage(
|
||||
context.resources.getString(R.string.github_url),
|
||||
context,
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(vertical = 5.dp),
|
||||
) {
|
||||
Row(
|
||||
@@ -269,7 +284,10 @@ fun SupportScreen(
|
||||
fontSize = 16.sp,
|
||||
modifier =
|
||||
Modifier.clickable {
|
||||
appViewModel.openWebPage(context.resources.getString(R.string.privacy_policy_url), context)
|
||||
appViewModel.openWebPage(
|
||||
context.resources.getString(R.string.privacy_policy_url),
|
||||
context,
|
||||
)
|
||||
},
|
||||
)
|
||||
Row(
|
||||
@@ -285,7 +303,7 @@ fun SupportScreen(
|
||||
val mode = buildAnnotatedString {
|
||||
append(stringResource(R.string.mode))
|
||||
append(": ")
|
||||
when(uiState.settings.isKernelEnabled){
|
||||
when (uiState.settings.isKernelEnabled) {
|
||||
true -> append(stringResource(id = R.string.kernel))
|
||||
false -> append(stringResource(id = R.string.userspace))
|
||||
}
|
||||
|
||||
+21
-8
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support.logs
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -8,7 +9,7 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -32,17 +33,17 @@ import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.zaneschepke.logcatter.model.LogMessage
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@Composable
|
||||
fun LogsScreen(appViewModel: AppViewModel) {
|
||||
fun LogsScreen(viewModel: LogsViewModel = hiltViewModel()) {
|
||||
|
||||
val logs = remember {
|
||||
appViewModel.logs
|
||||
}
|
||||
val logs = viewModel.logs
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
@@ -60,7 +61,15 @@ fun LogsScreen(appViewModel: AppViewModel) {
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
appViewModel.saveLogsToFile(context)
|
||||
scope.launch {
|
||||
viewModel.saveLogsToFile().onSuccess {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.logs_saved),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
@@ -82,7 +91,11 @@ fun LogsScreen(appViewModel: AppViewModel) {
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 24.dp),
|
||||
) {
|
||||
items(logs) {
|
||||
itemsIndexed(
|
||||
logs,
|
||||
key = { index, _ -> index },
|
||||
contentType = { _: Int, _: LogMessage -> null },
|
||||
) { _, it ->
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start),
|
||||
verticalAlignment = Alignment.Top,
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support.logs
|
||||
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.zaneschepke.logcatter.LocalLogCollector
|
||||
import com.zaneschepke.logcatter.model.LogMessage
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.module.MainDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.chunked
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class LogsViewModel
|
||||
@Inject constructor(
|
||||
private val localLogCollector: LocalLogCollector,
|
||||
private val fileUtils: FileUtils,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
@MainDispatcher private val mainDispatcher: CoroutineDispatcher
|
||||
) : ViewModel() {
|
||||
|
||||
val logs = mutableStateListOf<LogMessage>()
|
||||
|
||||
init {
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
localLogCollector.bufferedLogs.chunked(500, Duration.ofSeconds(1)).collect {
|
||||
withContext(mainDispatcher) {
|
||||
logs.addAll(it)
|
||||
}
|
||||
if (logs.size > Constants.LOG_BUFFER_SIZE) {
|
||||
withContext(mainDispatcher) {
|
||||
logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun saveLogsToFile(): Result<Unit> {
|
||||
val file = localLogCollector.getLogFile().getOrElse {
|
||||
return Result.failure(it)
|
||||
}
|
||||
val fileContent = fileUtils.readBytesFromFile(file)
|
||||
val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt"
|
||||
return fileUtils.saveByteArrayToDownloads(fileContent, fileName)
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,11 @@ object Constants {
|
||||
const val URI_CONTENT_SCHEME = "content"
|
||||
const val ALLOWED_FILE_TYPES = "*/*"
|
||||
const val TEXT_MIME_TYPE = "text/plain"
|
||||
const val ZIP_FILE_MIME_TYPE = "application/zip"
|
||||
const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs"
|
||||
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
||||
const val ALWAYS_ON_VPN_ACTION = "android.net.VpnService"
|
||||
const val EMAIL_MIME_TYPE = "message/rfc822"
|
||||
const val EMAIL_MIME_TYPE = "plain/text"
|
||||
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024
|
||||
|
||||
const val SUBSCRIPTION_TIMEOUT = 5_000L
|
||||
|
||||
@@ -9,13 +9,27 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.ObsoleteCoroutinesApi
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import kotlinx.coroutines.channels.produce
|
||||
import kotlinx.coroutines.channels.ticker
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.selects.whileSelect
|
||||
import org.amnezia.awg.config.Config
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
import java.text.DecimalFormat
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
fun BroadcastReceiver.goAsync(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
@@ -32,12 +46,6 @@ fun BroadcastReceiver.goAsync(
|
||||
}
|
||||
}
|
||||
|
||||
fun String.truncateWithEllipsis(allowedLength: Int): String {
|
||||
return if (this.length > allowedLength + 3) {
|
||||
this.substring(0, allowedLength) + "***"
|
||||
} else this
|
||||
}
|
||||
|
||||
fun BigDecimal.toThreeDecimalPlaceString(): String {
|
||||
val df = DecimalFormat("#.###")
|
||||
return df.format(this)
|
||||
@@ -73,14 +81,14 @@ fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus {
|
||||
}
|
||||
}
|
||||
|
||||
fun Config.toWgQuickString() : String {
|
||||
fun Config.toWgQuickString(): String {
|
||||
val amQuick = toAwgQuickString()
|
||||
val lines = amQuick.lines().toMutableList()
|
||||
val linesIterator = lines.iterator()
|
||||
while(linesIterator.hasNext()) {
|
||||
while (linesIterator.hasNext()) {
|
||||
val next = linesIterator.next()
|
||||
Constants.amneziaProperties.forEach {
|
||||
if(next.startsWith(it, ignoreCase = true)) {
|
||||
if (next.startsWith(it, ignoreCase = true)) {
|
||||
linesIterator.remove()
|
||||
}
|
||||
}
|
||||
@@ -88,9 +96,73 @@ fun Config.toWgQuickString() : String {
|
||||
return lines.joinToString(System.lineSeparator())
|
||||
}
|
||||
|
||||
fun Throwable.getMessage(context: Context) : String {
|
||||
return when(this) {
|
||||
fun Throwable.getMessage(context: Context): String {
|
||||
return when (this) {
|
||||
is WgTunnelExceptions -> this.getMessage(context)
|
||||
else -> this.message ?: StringValue.StringResource(R.string.unknown_error).asString(context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunks based on a time or size threshold.
|
||||
*
|
||||
* Borrowed from this [Stack Overflow question](https://stackoverflow.com/questions/51022533/kotlin-chunk-sequence-based-on-size-and-time).
|
||||
*/
|
||||
@OptIn(ObsoleteCoroutinesApi::class, ExperimentalCoroutinesApi::class)
|
||||
fun <T> ReceiveChannel<T>.chunked(scope: CoroutineScope, size: Int, time: Duration) =
|
||||
scope.produce<List<T>> {
|
||||
while (true) { // this loop goes over each chunk
|
||||
val chunk = ConcurrentLinkedQueue<T>() // current chunk
|
||||
val ticker = ticker(time.toMillis()) // time-limit for this chunk
|
||||
try {
|
||||
whileSelect {
|
||||
ticker.onReceive {
|
||||
false // done with chunk when timer ticks, takes priority over received elements
|
||||
}
|
||||
this@chunked.onReceive {
|
||||
chunk += it
|
||||
chunk.size < size // continue whileSelect if chunk is not full
|
||||
}
|
||||
}
|
||||
} catch (e: ClosedReceiveChannelException) {
|
||||
Timber.e(e)
|
||||
return@produce
|
||||
} finally {
|
||||
ticker.cancel()
|
||||
if (chunk.isNotEmpty()) {
|
||||
send(chunk.toList())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class)
|
||||
fun <T> Flow<T>.chunked(size: Int, time: Duration) = channelFlow {
|
||||
coroutineScope {
|
||||
val channel = asChannel(this@chunked).chunked(this, size, time)
|
||||
try {
|
||||
while (!channel.isClosedForReceive) {
|
||||
send(channel.receive())
|
||||
}
|
||||
} catch (e: ClosedReceiveChannelException) {
|
||||
// Channel was closed by the flow completing, nothing to do
|
||||
Timber.w(e)
|
||||
} catch (e: CancellationException) {
|
||||
channel.cancel(e)
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
channel.cancel(CancellationException("Closing channel due to flow exception", e))
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
fun <T> CoroutineScope.asChannel(flow: Flow<T>): ReceiveChannel<T> = produce {
|
||||
flow.collect { value ->
|
||||
channel.send(value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,20 +6,102 @@ import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.provider.MediaStore.MediaColumns
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.OutputStream
|
||||
import java.time.Instant
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
object FileUtils {
|
||||
private const val ZIP_FILE_MIME_TYPE = "application/zip"
|
||||
class FileUtils(
|
||||
private val context: Context,
|
||||
private val ioDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
|
||||
suspend fun readBytesFromFile(file: File): ByteArray {
|
||||
return withContext(ioDispatcher) {
|
||||
FileInputStream(file).use {
|
||||
it.readBytes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun readTextFromFileName(fileName: String): String {
|
||||
return withContext(ioDispatcher) {
|
||||
context.assets.open(fileName).use { stream ->
|
||||
stream.bufferedReader(Charsets.UTF_8).use {
|
||||
it.readText()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun saveByteArrayToDownloads(content: ByteArray, fileName: String): Result<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
|
||||
private fun createDownloadsFileOutputStream(
|
||||
context: Context,
|
||||
fileName: String,
|
||||
mimeType: String = Constants.ALLOWED_FILE_TYPES
|
||||
): OutputStream? {
|
||||
@@ -45,53 +127,4 @@ object FileUtils {
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun saveFileToDownloads(context: Context, content: String, fileName: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaColumns.DISPLAY_NAME, fileName)
|
||||
put(MediaColumns.MIME_TYPE, Constants.TEXT_MIME_TYPE)
|
||||
put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||
}
|
||||
val resolver = context.contentResolver
|
||||
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
||||
if (uri != null) {
|
||||
resolver.openOutputStream(uri).use { output ->
|
||||
output?.write(content.toByteArray())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val target = File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||
fileName,
|
||||
)
|
||||
FileOutputStream(target).use { output ->
|
||||
output.write(content.toByteArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveFilesToZip(context: Context, files: List<File>) : Result<Unit> {
|
||||
return try {
|
||||
val zipOutputStream =
|
||||
createDownloadsFileOutputStream(
|
||||
context,
|
||||
"wg-export_${Instant.now().epochSecond}.zip",
|
||||
ZIP_FILE_MIME_TYPE,
|
||||
)
|
||||
ZipOutputStream(zipOutputStream).use { zos ->
|
||||
files.forEach { file ->
|
||||
val entry = ZipEntry(file.name)
|
||||
zos.putNextEntry(entry)
|
||||
if (file.isFile) {
|
||||
file.inputStream().use { fis -> fis.copyTo(zos) }
|
||||
}
|
||||
}
|
||||
return Result.success(Unit)
|
||||
}
|
||||
} catch (e : Exception) {
|
||||
Timber.e(e)
|
||||
Result.failure(WgTunnelExceptions.ConfigExportFailed())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,121 +4,127 @@ import android.content.Context
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
|
||||
sealed class WgTunnelExceptions : Exception() {
|
||||
abstract fun getMessage(context: Context) : String
|
||||
data class General(private val userMessage : StringValue) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context) : String {
|
||||
abstract fun getMessage(context: Context): String
|
||||
data class General(private val userMessage: StringValue) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context): String {
|
||||
return userMessage.asString(context)
|
||||
}
|
||||
}
|
||||
|
||||
data class SsidConflict(private val userMessage : StringValue = StringValue.StringResource(R.string.error_ssid_exists)) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context) : String {
|
||||
data class SsidConflict(private val userMessage: StringValue = StringValue.StringResource(R.string.error_ssid_exists)) :
|
||||
WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context): String {
|
||||
return userMessage.asString(context)
|
||||
}
|
||||
}
|
||||
|
||||
data class ConfigExportFailed(private val userMessage : StringValue = StringValue.StringResource(R.string.export_configs_failed)) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context) : String {
|
||||
data class ConfigExportFailed(
|
||||
private val userMessage: StringValue = StringValue.StringResource(
|
||||
R.string.export_configs_failed,
|
||||
)
|
||||
) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context): String {
|
||||
return userMessage.asString(context)
|
||||
}
|
||||
}
|
||||
|
||||
data class ConfigParseError(private val appendMessage : StringValue = StringValue.Empty) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context) : String {
|
||||
data class ConfigParseError(private val appendMessage: StringValue = StringValue.Empty) :
|
||||
WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context): String {
|
||||
return StringValue.StringResource(R.string.config_parse_error).asString(context) + (
|
||||
if (appendMessage != StringValue.Empty) ": ${appendMessage.asString(context)}" else "")
|
||||
}
|
||||
}
|
||||
|
||||
data class RootDenied(private val userMessage : StringValue = StringValue.StringResource(R.string.error_root_denied)) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context) : String {
|
||||
data class RootDenied(private val userMessage: StringValue = StringValue.StringResource(R.string.error_root_denied)) :
|
||||
WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context): String {
|
||||
return userMessage.asString(context)
|
||||
}
|
||||
}
|
||||
|
||||
data class InvalidQrCode(private val userMessage : StringValue = StringValue.StringResource(R.string.error_invalid_code)) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context) : String {
|
||||
data class InvalidQrCode(private val userMessage: StringValue = StringValue.StringResource(R.string.error_invalid_code)) :
|
||||
WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context): String {
|
||||
return userMessage.asString(context)
|
||||
}
|
||||
}
|
||||
|
||||
data class InvalidFileExtension(private val userMessage : StringValue = StringValue.StringResource(R.string.error_file_extension)) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context) : String {
|
||||
data class InvalidFileExtension(
|
||||
private val userMessage: StringValue = StringValue.StringResource(
|
||||
R.string.error_file_extension,
|
||||
)
|
||||
) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context): String {
|
||||
return userMessage.asString(context)
|
||||
}
|
||||
}
|
||||
|
||||
data class FileReadFailed(private val userMessage : StringValue = StringValue.StringResource(R.string.error_file_format)) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context) : String {
|
||||
data class FileReadFailed(private val userMessage: StringValue = StringValue.StringResource(R.string.error_file_format)) :
|
||||
WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context): String {
|
||||
return userMessage.asString(context)
|
||||
}
|
||||
}
|
||||
|
||||
data class AuthenticationFailed(private val userMessage : StringValue = StringValue.StringResource(R.string.error_authentication_failed)) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context) : String {
|
||||
data class AuthenticationFailed(
|
||||
private val userMessage: StringValue = StringValue.StringResource(
|
||||
R.string.error_authentication_failed,
|
||||
)
|
||||
) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context): String {
|
||||
return userMessage.asString(context)
|
||||
}
|
||||
}
|
||||
|
||||
data class AuthorizationFailed(private val userMessage : StringValue = StringValue.StringResource(R.string.error_authorization_failed)) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context) : String {
|
||||
data class AuthorizationFailed(
|
||||
private val userMessage: StringValue = StringValue.StringResource(
|
||||
R.string.error_authorization_failed,
|
||||
)
|
||||
) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context): String {
|
||||
return userMessage.asString(context)
|
||||
}
|
||||
}
|
||||
|
||||
data class BackgroundLocationRequired(private val userMessage : StringValue = StringValue.StringResource(R.string.background_location_required)) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context) : String {
|
||||
data class BackgroundLocationRequired(
|
||||
private val userMessage: StringValue = StringValue.StringResource(
|
||||
R.string.background_location_required,
|
||||
)
|
||||
) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context): String {
|
||||
return userMessage.asString(context)
|
||||
}
|
||||
}
|
||||
|
||||
data class LocationServicesRequired(private val userMessage : StringValue = StringValue.StringResource(R.string.location_services_required)) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context) : String {
|
||||
data class LocationServicesRequired(
|
||||
private val userMessage: StringValue = StringValue.StringResource(
|
||||
R.string.location_services_required,
|
||||
)
|
||||
) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context): String {
|
||||
return userMessage.asString(context)
|
||||
}
|
||||
}
|
||||
|
||||
data class PreciseLocationRequired(private val userMessage : StringValue = StringValue.StringResource(R.string.precise_location_required)) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context) : String {
|
||||
data class PreciseLocationRequired(
|
||||
private val userMessage: StringValue = StringValue.StringResource(
|
||||
R.string.precise_location_required,
|
||||
)
|
||||
) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context): String {
|
||||
return userMessage.asString(context)
|
||||
}
|
||||
}
|
||||
|
||||
data class FileExplorerRequired (private val userMessage : StringValue = StringValue.StringResource(R.string.error_no_file_explorer)) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context) : String {
|
||||
data class FileExplorerRequired(
|
||||
private val userMessage: StringValue = StringValue.StringResource(
|
||||
R.string.error_no_file_explorer,
|
||||
)
|
||||
) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context): String {
|
||||
return userMessage.asString(context)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// sealed class Message : Event() {
|
||||
// data object ConfigSaved : Message() {
|
||||
// override val message: String
|
||||
// get() = WireGuardAutoTunnel.instance.getString(R.string.config_changes_saved)
|
||||
// }
|
||||
//
|
||||
// data object ConfigsExported : Message() {
|
||||
// override val message: String
|
||||
// get() = WireGuardAutoTunnel.instance.getString(R.string.exported_configs_message)
|
||||
// }
|
||||
//
|
||||
// data object TunnelOffAction : Message() {
|
||||
// override val message: String
|
||||
// get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_tunnel)
|
||||
// }
|
||||
//
|
||||
// data object TunnelOnAction : Message() {
|
||||
// override val message: String
|
||||
// get() = WireGuardAutoTunnel.instance.getString(R.string.turn_on_tunnel)
|
||||
// }
|
||||
//
|
||||
// data object AutoTunnelOffAction : Message() {
|
||||
// override val message: String
|
||||
// get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_auto)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:pathData="M440,520L200,520v-80h240v-240h80v240h240v80L520,520v240h-80v-240Z"
|
||||
android:fillColor="#e8eaed"/>
|
||||
<path
|
||||
android:fillColor="#e8eaed"
|
||||
android:pathData="M440,520L200,520v-80h240v-240h80v240h240v80L520,520v240h-80v-240Z" />
|
||||
</vector>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<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"/>
|
||||
<path
|
||||
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>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<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"/>
|
||||
<path
|
||||
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>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
android:height="50dp"
|
||||
android:viewportWidth="50"
|
||||
android:viewportHeight="50">
|
||||
<path
|
||||
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"/>
|
||||
<path
|
||||
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" />
|
||||
</vector>
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="no_tunnels">Žádné tunely ještě nebyly přidány!</string>
|
||||
<string name="watcher_notification_text_paused">Monitorování změn ve stavu sítě: pozastaveno</string>
|
||||
<string name="notification_permission_required">Je vyžadováno oprávnění pro oznámení.</string>
|
||||
<string name="add_trusted_ssid">Přidat název důvěryhodné Wi-Fi</string>
|
||||
<string name="enable_auto_tunnel">Spustit automatické tunelování</string>
|
||||
<string name="tunnel_mobile_data">Tunelování na mobilních datech</string>
|
||||
<string name="one_tunnel_required">Alespoň jeden tunel je vyžadován pro použití této funkce</string>
|
||||
<string name="privacy_policy">Otevřít zásady soukromí</string>
|
||||
<string name="okay">OK</string>
|
||||
<string name="thank_you">Děkujeme za používání WG Tunnel!</string>
|
||||
<string name="add_tunnels_text">Přidat ze souboru nebo zipu</string>
|
||||
<string name="open_file">Otevřít soubor</string>
|
||||
<string name="add_from_qr">Přidat z QR kódu</string>
|
||||
<string name="qr_scan">QR skenování</string>
|
||||
<string name="tunnel_name">Název tunelu</string>
|
||||
<string name="add_tunnel">Přidat tunel</string>
|
||||
<string name="exclude">Vyloučit</string>
|
||||
<string name="include">Zahrnout</string>
|
||||
<string name="save_changes">Uložit</string>
|
||||
<string name="endpoint">Koncový bod</string>
|
||||
<string name="vpn_connection_failed">Připojení selhalo</string>
|
||||
<string name="always_on_vpn_support">Povolit trvalé připojení VPN</string>
|
||||
<string name="location_services_not_detected">Služby polohy nebyly detekovány</string>
|
||||
<string name="hint_search_packages">Hledat balíčky</string>
|
||||
<string name="attempt_connection">Pokus o připojení...</string>
|
||||
<string name="vpn_starting">VPN se spouští</string>
|
||||
<string name="vpn_on">VPN zapnuto</string>
|
||||
<string name="vpn_off">VPN vypnuto</string>
|
||||
<string name="default_vpn_on">Hlavní VPN zapnuto</string>
|
||||
<string name="default_vpn_off">Hlavní VPN vypnuto</string>
|
||||
<string name="create_import">Vytvořit od základu</string>
|
||||
<string name="turn_off_auto">Akce vyžaduje, aby bylo automatické tunelování vypnuté či pozastavené</string>
|
||||
<string name="add_peer">Přidat peer</string>
|
||||
<string name="comma_separated_list">seznam oddělený čárkami</string>
|
||||
<string name="random">(náhodné)</string>
|
||||
<string name="optional">(volitelné)</string>
|
||||
<string name="optional_no_recommend">(volitelné, nedoporučeno)</string>
|
||||
<string name="preshared_key">Předsdílený klíč</string>
|
||||
<string name="seconds">vteřin(y)</string>
|
||||
<string name="cancel">Zrušit</string>
|
||||
<string name="error_authentication_failed">Autentizace selhala</string>
|
||||
<string name="error_authorization_failed">Nepovedlo se autorizovat</string>
|
||||
<string name="enabled_app_shortcuts">Zapnout zkratky</string>
|
||||
<string name="export_configs">Exportovat konfigurace</string>
|
||||
<string name="export_configs_failed">Nepovedlo se exportovat konfigurace</string>
|
||||
<string name="location_services_required">Služby polohy vyžadovány</string>
|
||||
<string name="background_location_required">Oprávnění pro polohu na pozadí vyžadováno</string>
|
||||
<string name="precise_location_required">Oprávnění pro přesnou polohu vyžadováno</string>
|
||||
<string name="unknown_error">Došlo k neznámé chybě</string>
|
||||
<string name="exported_configs_message">Konfigurace exportovány do stažených souborů</string>
|
||||
<string name="tunnel_on_wifi">Tunelovat na nedůvěryhodné Wi-Fi</string>
|
||||
<string name="email_subject">WG Tunnel podpora</string>
|
||||
<string name="email_chooser">Poslat email…</string>
|
||||
<string name="use_kernel">Použít kernel modul</string>
|
||||
<string name="error_ssid_exists">SSID již existuje</string>
|
||||
<string name="error_root_denied">Oprávnění root zamítnuto</string>
|
||||
<string name="error_no_file_explorer">Žádný průzkumník souborů není nainstalován</string>
|
||||
<string name="error_invalid_code">Neplatný QR kód</string>
|
||||
<string name="error_none">Žádná chyba</string>
|
||||
<string name="auto_tunnel_title">Služba automatického tunelování</string>
|
||||
<string name="resume">Obnovit</string>
|
||||
<string name="active">aktivní</string>
|
||||
<string name="open_issue">Otevřít případ</string>
|
||||
<string name="incorrect_pin">PIN je nesprávný</string>
|
||||
<string name="create_pin">Vytvořte PIN</string>
|
||||
<string name="set_primary_tunnel">Nastavit jako hlavní tunel</string>
|
||||
<string name="use_tunnel_on_wifi_name">Použít tunel pro Wi-Fi</string>
|
||||
<string name="edit_tunnel">Upravit tunel</string>
|
||||
<string name="disabled">vypnuto</string>
|
||||
<string name="auto_tun_on">Obnovit automatické tunelování</string>
|
||||
<string name="auto_tun_off">Pozastavit automatické tunelování</string>
|
||||
<string name="version">Verze</string>
|
||||
<string name="mode">Mód</string>
|
||||
<string name="use_amnezia">"Použít Amnezia userspace "</string>
|
||||
<string name="junk_packet_count">Junk packet počet</string>
|
||||
<string name="junk_packet_minimum_size">Junk packet minimální velikost</string>
|
||||
<string name="junk_packet_maximum_size">Junk packet maximální velikost</string>
|
||||
<string name="init_packet_junk_size">Init packet junk velikost</string>
|
||||
<string name="response_packet_junk_size">Response packet junk velikost</string>
|
||||
<string name="unsure_how">pokud si nejste jisti, jak postupovat</string>
|
||||
<string name="see_the">Podívejte se na</string>
|
||||
<string name="getting_started_guide">začátečnickou příručku</string>
|
||||
<string name="error_file_format">Neplatný formát konfigurace</string>
|
||||
<string name="error_file_extension">Soubor není ve formátu .conf nebo .zip</string>
|
||||
<string name="turn_off_tunnel">Akce vyžaduje vypnutí tunelu</string>
|
||||
<string name="watcher_notification_text_active">Monitorování změn ve stavu sítě: aktivní</string>
|
||||
<string name="tunnel_on_ethernet">Tunelovat na ethernetu</string>
|
||||
<string name="tunnel_start_title">VPN připojeno</string>
|
||||
<string name="prominent_background_location_message">Tato funkce vyžaduje oprávnění pro přístup k poloze na pozadí pro zapnutí monitorování Wi-Fi SSID, i když je aplikace zavřená. Pro více detailů, podívejte se prosím na zásady soukromí umístěné v kategorii Podpora.</string>
|
||||
<string name="tunnel_start_text">Připojeno k tunelu</string>
|
||||
<string name="tunnels">Tunely</string>
|
||||
<string name="disable_auto_tunnel">Zastavit automatické tunelování</string>
|
||||
<string name="tunnel_all">Tunelovat všechny aplikace</string>
|
||||
<string name="config_changes_saved">Změny v konfiguraci uloženy.</string>
|
||||
<string name="icon">Ikona</string>
|
||||
<string name="no_thanks">Ne, děkuji</string>
|
||||
<string name="turn_on">Zapnout</string>
|
||||
<string name="map">Mapa</string>
|
||||
<string name="public_key">Veřejný klíč</string>
|
||||
<string name="addresses">Adresy</string>
|
||||
<string name="dns_servers">DNS servery</string>
|
||||
<string name="allowed_ips">Povolené IP adresy</string>
|
||||
<string name="name">Název</string>
|
||||
<string name="restart">Restartovat tunel</string>
|
||||
<string name="scanning_qr">Skenování QR</string>
|
||||
<string name="none">Žádné názvy důvěryhodných Wi-Fi</string>
|
||||
<string name="other">Ostatní</string>
|
||||
<string name="auto_tunneling">Automatické tunelování</string>
|
||||
<string name="turn_on_tunnel">Akce vyžaduje aktivní tunel</string>
|
||||
<string name="interface_">Rozhraní</string>
|
||||
<string name="done">Hotovo</string>
|
||||
<string name="rotate_keys">Rotovat klíče</string>
|
||||
<string name="private_key">Soukromý klíč</string>
|
||||
<string name="copy_public_key">Kopírovat veřejný klíč</string>
|
||||
<string name="base64_key">base64 klíč</string>
|
||||
<string name="docs_description">Přečíst si dokumentaci</string>
|
||||
<string name="discord_description">Přidat se ke komunitě</string>
|
||||
<string name="email_description">Poslat mi email</string>
|
||||
<string name="support_help_text">Pokud máte potíže, nápady pro zlepšení, nebo se chcete jen zapojit, následující prostředky jsou k dispozici:</string>
|
||||
<string name="location_services_missing_message">Aplikace nenašla žádné služby polohy zapnuté na Vašem zařízení. Dle Vašeho zařízení, tohle může způsobit, že funkce nedůvěryhodné Wi-Fi nedokáže přečíst jméno připojené Wi-Fi. Chcete i přesto pokračovat?</string>
|
||||
<string name="delete_tunnel">Smazat tunel</string>
|
||||
<string name="delete_tunnel_message">Jste si jisti, že chcete smazat tento tunel?</string>
|
||||
<string name="yes">Ano</string>
|
||||
<string name="pause">Pozastavit</string>
|
||||
<string name="paused">pozastaveno</string>
|
||||
<string name="tunneling_apps">Tunelování aplikací</string>
|
||||
<string name="all">vše</string>
|
||||
<string name="included">zahrnuto</string>
|
||||
<string name="excluded">vyloučeno</string>
|
||||
<string name="always_on_disabled">Trvalé VPN připojení se pokusilo spustit tunel, ale tato funkce je vypnutá v nastavení.</string>
|
||||
<string name="no_email_detected">Žádná emailová aplikace nebyla nalezena</string>
|
||||
<string name="no_browser_detected">Žádný prohlížeč nebyl nalezen</string>
|
||||
<string name="logs_saved">Logy uloženy do stažených souborů</string>
|
||||
<string name="read_logs">Přečíst si logy</string>
|
||||
<string name="config_parse_error">Nepovedlo se vložit konfiguraci</string>
|
||||
<string name="pin_created">PIN úspěšně vytvořen</string>
|
||||
<string name="enter_pin">Vložte Váš PIN</string>
|
||||
<string name="no_wifi_names_configured">Žádné názvy Wi-Fi nebyly nastaveny pro tento tunel</string>
|
||||
<string name="enable_app_lock">Zapnout zámek aplikace</string>
|
||||
<string name="restart_on_ping">Restartovat při selhání pingu</string>
|
||||
<string name="mobile_data_tunnel">Nastavit jako tunel pro mobilní data</string>
|
||||
<string name="general">Obecné</string>
|
||||
<string name="settings">Nastavení</string>
|
||||
<string name="support">Podpora</string>
|
||||
</resources>
|
||||
@@ -1,42 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">WG Tunnel</string>
|
||||
<string name="error_file_extension">Dies ist keine .conf oder .zip Datei</string>
|
||||
<string name="error_file_extension">Datei ist keine .conf oder .zip</string>
|
||||
<string name="no_tunnels">Noch keine Tunnel hinzugefügt!</string>
|
||||
<string name="watcher_notification_text_active">Überwachung der Netzwerkänderungen: aktiv</string>
|
||||
<string name="tunnels">Tunnel</string>
|
||||
<string name="enable_auto_tunnel">Starte automatisches Verbinden</string>
|
||||
<string name="tunnel_mobile_data">Verbinden im Mobilnetzwerk</string>
|
||||
<string name="one_tunnel_required">Mindestens eine Verbindung wird für diese Funktion benötigt</string>
|
||||
<string name="privacy_policy">Siehe Privacy Policy</string>
|
||||
<string name="disable_auto_tunnel">Stoppe automatisches Verbinden</string>
|
||||
<string name="okay">Okay</string>
|
||||
<string name="tunnel_on_ethernet">Verbindung über Ethernet</string>
|
||||
<string name="search_icon">Suchsymbol</string>
|
||||
<string name="attempt_connection">Versuche zu verbinden..</string>
|
||||
<string name="auto_tunneling">Automatisches verbinden</string>
|
||||
<string name="enable_auto_tunnel">Auto-Tunneln starten</string>
|
||||
<string name="tunnel_mobile_data">Tunnel für mobile Daten</string>
|
||||
<string name="one_tunnel_required">Mindestens ein Tunnel wird für diese Funktion benötigt</string>
|
||||
<string name="privacy_policy">Datenschutzbestimmungen anzeigen</string>
|
||||
<string name="disable_auto_tunnel">Auto-Tunneln stoppen</string>
|
||||
<string name="okay">Ok</string>
|
||||
<string name="tunnel_on_ethernet">Tunnel für Ethernet</string>
|
||||
<string name="attempt_connection">Verbindungsversuch..</string>
|
||||
<string name="auto_tunneling">Auto-Tunneln</string>
|
||||
<string name="default_vpn_off">Primärer VPN aus</string>
|
||||
<string name="turn_on_tunnel">Für diese Altion muss eine Verbindung bestehen</string>
|
||||
<string name="turn_on_tunnel">Für diese Aktion muss ein aktiver Tunnel bestehen</string>
|
||||
<string name="watcher_notification_text_paused">Überwachung der Netzwerkänderungen: pausiert</string>
|
||||
<string name="tunnel_start_title">VPN verbunden</string>
|
||||
<string name="tunnel_start_text">Mit Tunnel verbunden</string>
|
||||
<string name="notification_permission_required">Berechtigung für die Benachrichtigung benötigt.</string>
|
||||
<string name="add_trusted_ssid">Vertrauenswürdiger WiFi-Name hinzugügen</string>
|
||||
<string name="prominent_background_location_message">Diese Funktion benötigt Standortberechtigung im Hintergrund um die WiFi SSIDs auch wenn die Applikation geschlossen ist zu überwachen. Für mehr Infos, siehe Privacy Policy auf der Hilfeseite.</string>
|
||||
<string name="notification_permission_required">Benachrichtigungsberechtigung benötigt.</string>
|
||||
<string name="add_trusted_ssid">Vertrauenswürdigen WLAN-Namen hinzufügen</string>
|
||||
<string name="prominent_background_location_message">Diese Funktion erfordert die Erlaubnis zur Standortbestimmung im Hintergrund, um die Überwachung der WLAN SSID zu ermöglichen, auch wenn die Anwendung geschlossen ist. Weitere Einzelheiten in den Datenschutzbestimmungen, die auf dem Support-Bildschirm verlinkt sind.</string>
|
||||
<string name="prominent_background_location_title">Vereinbarung der Standortberechtigung im Hintergrund</string>
|
||||
<string name="thank_you">Danke fürs benutzen von WG Tunnel!</string>
|
||||
<string name="thank_you">Danke fürs Benutzen von WG Tunnel!</string>
|
||||
<string name="trusted_ssid_empty_description">SSID eingeben</string>
|
||||
<string name="trusted_ssid_value_description">SSID bestätigen</string>
|
||||
<string name="add_tunnels_text">Von Datei oder ZIP hinzufügen</string>
|
||||
<string name="open_file">Datei geöffnet</string>
|
||||
<string name="add_from_qr">Von QR-Code hinzufügen</string>
|
||||
<string name="open_file">Datei öffnen</string>
|
||||
<string name="add_from_qr">Über QR-Code hinzufügen</string>
|
||||
<string name="qr_scan">Scanne QR</string>
|
||||
<string name="tunnel_name">Verbindungsname</string>
|
||||
<string name="add_tunnel">Verbindung hinzufügen</string>
|
||||
<string name="exclude">Ausgenommen</string>
|
||||
<string name="include">Eingeschlossen</string>
|
||||
<string name="tunnel_all">Verbinde alle Apps</string>
|
||||
<string name="config_changes_saved">Änderungen der Konfiguration gespeichert.</string>
|
||||
<string name="tunnel_name">Tunnel Name</string>
|
||||
<string name="add_tunnel">Tunnel hinzufügen</string>
|
||||
<string name="exclude">Ausschließen</string>
|
||||
<string name="include">Einschließen</string>
|
||||
<string name="tunnel_all">Alle Apps tunneln</string>
|
||||
<string name="config_changes_saved">Konfigurationsänderungen gespeichert.</string>
|
||||
<string name="save_changes">Speichern</string>
|
||||
<string name="icon">Symbol</string>
|
||||
<string name="no_thanks">Nein danke</string>
|
||||
@@ -50,52 +49,51 @@
|
||||
<string name="allowed_ips">Erlaubte IPs</string>
|
||||
<string name="endpoint">Endpunkt</string>
|
||||
<string name="name">Name</string>
|
||||
<string name="restart">Verbindung neustarten</string>
|
||||
<string name="restart">Tunnel neustarten</string>
|
||||
<string name="vpn_connection_failed">Verbindung fehlgeschlagen</string>
|
||||
<string name="always_on_vpn_support">Erlaube Always-On VPN</string>
|
||||
<string name="location_services_not_detected">Standortdienste Nicht Erkannt</string>
|
||||
<string name="hint_search_packages">Suche packete</string>
|
||||
<string name="clear_icon">Lösche Symbol</string>
|
||||
<string name="always_on_vpn_support">Always-On VPN erlauben</string>
|
||||
<string name="location_services_not_detected">Standortdienste nicht erkannt</string>
|
||||
<string name="hint_search_packages">Pakete suchen</string>
|
||||
<string name="vpn_starting">VPN startet</string>
|
||||
<string name="db_name">wg-tunnel-db</string>
|
||||
<string name="scanning_qr">Scanne nach QR</string>
|
||||
<string name="none">Keine vertrauenswürdigen WiFi Namen</string>
|
||||
<string name="other">Andere</string>
|
||||
<string name="none">Keine vertrauenswürdigen WLAN Namen</string>
|
||||
<string name="other">Sonstige</string>
|
||||
<string name="vpn_on">VPN an</string>
|
||||
<string name="vpn_off">VPN aus</string>
|
||||
<string name="default_vpn_on">Primärer VPN an</string>
|
||||
<string name="create_import">Starte von Grund auf neu</string>
|
||||
<string name="turn_off_auto">Für diese Aktion muss automatisches Verbinden ausgeschaltet oder pausiert sein</string>
|
||||
<string name="create_import">Von Grund auf neu erstellen</string>
|
||||
<string name="turn_off_auto">Für diese Aktion muss Auto-Tunneln ausgeschaltet oder pausiert sein</string>
|
||||
<string name="add_peer">Peer hinzufügen</string>
|
||||
<string name="done">Erledigt</string>
|
||||
<string name="rotate_keys">Schlüssel ändern</string>
|
||||
<string name="rotate_keys">Schlüssel rotieren</string>
|
||||
<string name="private_key">Privater Schlüssel</string>
|
||||
<string name="copy_public_key">Öffentlicher Schlüssel kopieren</string>
|
||||
<string name="copy_public_key">Öffentlichen Schlüssel kopieren</string>
|
||||
<string name="base64_key">base64-Schlüssel</string>
|
||||
<string name="comma_separated_list">Kommaseparierte Liste</string>
|
||||
<string name="delete_tunnel">Tunnel löschen</string>
|
||||
<string name="persistent_keepalive">Dauerhaftes Keepalive</string>
|
||||
<string name="background_location_required">Hintergrund Standortdienste erforderlich</string>
|
||||
<string name="enable_app_lock">App Sperre aktiviert</string>
|
||||
<string name="enable_app_lock">App-Sperre aktivieren</string>
|
||||
<string name="discord_description">Tritt der Community bei</string>
|
||||
<string name="interface_">Schnittstelle</string>
|
||||
<string name="listen_port">Eingehender Port</string>
|
||||
<string name="random">(zufällig)</string>
|
||||
<string name="optional">(nicht erforderlich)</string>
|
||||
<string name="optional_no_recommend">(nicht erforderlich, aber empfohlen)</string>
|
||||
<string name="optional">(optional)</string>
|
||||
<string name="optional_no_recommend">(Optional, nicht empfohlen)</string>
|
||||
<string name="seconds">Sekunden</string>
|
||||
<string name="cancel">Abbrechen</string>
|
||||
<string name="preshared_key">Geteilter Schlüssel</string>
|
||||
<string name="enabled_app_shortcuts">Aktiviere App Verknüpfungen</string>
|
||||
<string name="exported_configs_message">Konfigurationen in den Download Ordner exportiert</string>
|
||||
<string name="tunnel_on_wifi">Tunnel bei nicht vertrauenswürdigem Wifi</string>
|
||||
<string name="enabled_app_shortcuts">App-Verknüpfungen aktivieren</string>
|
||||
<string name="exported_configs_message">Konfigurationen in Download Ordner exportiert</string>
|
||||
<string name="tunnel_on_wifi">Tunnel bei nicht vertrauenswürdigem WLAN</string>
|
||||
<string name="email_subject">WG Tunnel Unterstützung</string>
|
||||
<string name="docs_description">Lese die Dokumentation</string>
|
||||
<string name="docs_description">Dokumentation lesen</string>
|
||||
<string name="email_description">Sende mir eine E-Mail</string>
|
||||
<string name="support_help_text">Bei Fehlern oder Verbesserungsvorschlägen stehen folgende Ressourcen zur Verfügung:</string>
|
||||
<string name="error_root_denied">Root Shell verboten</string>
|
||||
<string name="error_root_denied">Root Shell verweigert</string>
|
||||
<string name="error_no_file_explorer">Kein Datei-Explorer installiert</string>
|
||||
<string name="location_services_missing_message">Die App konnte keine aktivierten Standortdienste auf deinem Gerät erkennen. Dies kann auf manchen Geräten ein Auslesen des aktuellen WIFI-Names für die \"Nicht vertrauenswürdiges WIFI\" Funktion verhindern. Möchtest du trotzdem fortfahren ?</string>
|
||||
<string name="location_services_missing_message">Die App erkennt keine auf deinem Gerät aktivierten Standortdienste. Je nach Gerät kann dies dazu führen, dass die Funktion \"Nicht vertrauenswürdiges WLAN\" den WLAN-Namen nicht lesen kann. Möchtest du trotzdem fortfahren?</string>
|
||||
<string name="auto_tunnel_title">Auto-Tunnel Service</string>
|
||||
<string name="delete_tunnel_message">Bist du sicher, dass du den Tunnel löschen möchtest?</string>
|
||||
<string name="yes">Ja</string>
|
||||
@@ -103,62 +101,71 @@
|
||||
<string name="pause">Pausieren</string>
|
||||
<string name="paused">Pausiert</string>
|
||||
<string name="active">Aktiv</string>
|
||||
<string name="go">Gehe</string>
|
||||
<string name="excluded">Ausgeschlossen</string>
|
||||
<string name="go">Los</string>
|
||||
<string name="excluded">ausgeschlossen</string>
|
||||
<string name="all">Alle</string>
|
||||
<string name="always_on_disabled">Always-on VPN wollte eine Tunnel starten, aber dieses Feature ist in den Einstellungen deaktiviert.</string>
|
||||
<string name="no_browser_detected">Kein Browser erkannt</string>
|
||||
<string name="open_issue">Öffne ein Issue</string>
|
||||
<string name="read_logs">Lese die Logs</string>
|
||||
<string name="always_on_disabled">Always-on VPN wollte einen Tunnel starten, aber dieses Feature ist in den Einstellungen deaktiviert.</string>
|
||||
<string name="no_browser_detected">Keinen Browser erkannt</string>
|
||||
<string name="open_issue">Issue öffnen</string>
|
||||
<string name="read_logs">Logs lesen</string>
|
||||
<string name="auto">(automatisch)</string>
|
||||
<string name="config_parse_error">Fehler beim lesen der Konfiguration</string>
|
||||
<string name="incorrect_pin">PIN is nicht korrekt</string>
|
||||
<string name="pin_created">PIN erfolgreich angelegt</string>
|
||||
<string name="enter_pin">Gib deine PIN ein</string>
|
||||
<string name="auto_off">Auto-Tunnel pausieren</string>
|
||||
<string name="auto_tun_on">Auto-Tunnel fortsetzen</string>
|
||||
<string name="auto_tun_off">Auto-Tunnel pausieren</string>
|
||||
<string name="config_parse_error">Fehler beim Lesen der Konfiguration</string>
|
||||
<string name="incorrect_pin">PIN nicht korrekt</string>
|
||||
<string name="pin_created">PIN erfolgreich erstellt</string>
|
||||
<string name="enter_pin">Deine PIN eingeben</string>
|
||||
<string name="auto_off">Auto-Tunneln pausieren</string>
|
||||
<string name="auto_tun_on">Auto-Tunneln fortsetzen</string>
|
||||
<string name="auto_tun_off">Auto-Tunneln pausieren</string>
|
||||
<string name="version">Version</string>
|
||||
<string name="mode">Modus</string>
|
||||
<string name="userspace">Userspace</string>
|
||||
<string name="userspace">Benutzerfläche</string>
|
||||
<string name="settings">Einstellungen</string>
|
||||
<string name="support">Unterstützung</string>
|
||||
<string name="watcher_channel_id">Beobachter Kanal</string>
|
||||
<string name="watcher_channel_id">Wächterkanal</string>
|
||||
<string name="error_authentication_failed">Anmeldung fehlgeschlagen</string>
|
||||
<string name="export_configs">Exportiere Konfigurationen</string>
|
||||
<string name="unknown_error">Ein unbekannter Fehler ist aufgetreten</string>
|
||||
<string name="export_configs">Konfigurationen exportieren</string>
|
||||
<string name="unknown_error">Unbekannter Fehler aufgetreten</string>
|
||||
<string name="email_chooser">Sende eine E-Mail…</string>
|
||||
<string name="error_authorization_failed">Fehler bei der Authorisierung</string>
|
||||
<string name="error_authorization_failed">Autorisierung fehlgeschlagen</string>
|
||||
<string name="location_services_required">Standortdienste erforderlich</string>
|
||||
<string name="precise_location_required">Genauer Standort erforderlich</string>
|
||||
<string name="error_invalid_code">Fehlerhafter QR code</string>
|
||||
<string name="error_invalid_code">Ungültiger QR Code</string>
|
||||
<string name="error_none">Kein Fehler</string>
|
||||
<string name="tunneling_apps">Tunnel Anwendungen</string>
|
||||
<string name="included">Hinzugefügt</string>
|
||||
<string name="no_email_detected">Keine E-Mail Anwendung erkannt</string>
|
||||
<string name="tunneling_apps">Getunnelte Apps</string>
|
||||
<string name="included">eingeschlossen</string>
|
||||
<string name="no_email_detected">Keine E-Mail-App erkannt</string>
|
||||
<string name="logs_saved">Logs im Download Ordner gespeichert</string>
|
||||
<string name="create_pin">Erstelle eine PIN</string>
|
||||
<string name="use_tunnel_on_wifi_name">Tunnel in Wifi-Namen verwenden</string>
|
||||
<string name="no_wifi_names_configured">Keine Wifi-Namen für diesen Tunnel konfiguriert</string>
|
||||
<string name="create_pin">PIN erstellen</string>
|
||||
<string name="use_tunnel_on_wifi_name">Tunnel für WLAN-Namen verwenden</string>
|
||||
<string name="no_wifi_names_configured">Keine WLAN-Namen für diesen Tunnel konfiguriert</string>
|
||||
<string name="disabled">Deaktiviert</string>
|
||||
<string name="mobile_data_tunnel">Als Tunnel für Mobile Daten setzen</string>
|
||||
<string name="general">Allgemein</string>
|
||||
<string name="restart_on_ping">Neustart bei PING Fehler (Beta)</string>
|
||||
<string name="edit_tunnel">Tunnel bearbeiten</string>
|
||||
<string name="set_primary_tunnel">Als Primären Tunnel setzen</string>
|
||||
<string name="auto_on">Auto-Tunnel fortsetzen</string>
|
||||
<string name="auto_on">Auto-Tunneln fortsetzen</string>
|
||||
<string name="vpn_channel_id">VPN Kanal</string>
|
||||
<string name="vpn_channel_name">VPN Benachrichtigungskanal</string>
|
||||
<string name="watcher_channel_name">Beobachter Benachrichtigungskanal</string>
|
||||
<string name="watcher_channel_name">Wächterbenachrichtigungskanal</string>
|
||||
<string name="turn_off_tunnel">Aktion erfordert deaktivierten Tunnel</string>
|
||||
<string name="kernel">Kernel</string>
|
||||
<string name="use_kernel">Verwende das Kernel Modul</string>
|
||||
<string name="use_kernel">Kernelmodul verwenden</string>
|
||||
<string name="error_ssid_exists">SSID existiert bereits</string>
|
||||
<string name="use_amnezia">"Benutze Amnezia Benuzterumgebung "</string>
|
||||
<string name="junk_packet_count">Müll Packet Anzahk</string>
|
||||
<string name="junk_packet_maximum_size">Müll Packet maximale Grösse</string>
|
||||
<string name="init_packet_junk_size">Erstes Packet Müllgrösse</string>
|
||||
<string name="use_amnezia">"Amnezia Benutzerumgebung benutzen "</string>
|
||||
<string name="junk_packet_count">Junk-Paket Anzahl</string>
|
||||
<string name="junk_packet_maximum_size">Junk-Paket maximale Grösse</string>
|
||||
<string name="init_packet_junk_size">Initial Junk-Paketgröße</string>
|
||||
<string name="backend">Backend</string>
|
||||
<string name="junk_packet_minimum_size">Müll Packet minimale Grösse</string>
|
||||
<string name="response_packet_junk_size">Antwortpaket Müllgrösse</string>
|
||||
<string name="junk_packet_minimum_size">Junk-Paket minimale Grösse</string>
|
||||
<string name="response_packet_junk_size">Antwort Junk-Paketgröße</string>
|
||||
<string name="init_packet_magic_header">Initialpaket magic header</string>
|
||||
<string name="getting_started_guide">Startanleitung erhalten</string>
|
||||
<string name="transport_packet_magic_header">Transportpaket magic header</string>
|
||||
<string name="underload_packet_magic_header">Unterlastpaket magic header</string>
|
||||
<string name="see_the">Schaue das</string>
|
||||
<string name="unsure_how">Wenn du nicht sicher bist, wie du weiterverfahren sollst</string>
|
||||
<string name="export_configs_failed">Konfigurationsexport fehlgeschlagen</string>
|
||||
<string name="error_file_format">Ungültige Konfiguration Tunnel-Format</string>
|
||||
<string name="response_packet_magic_header">Antwortpaket magic header</string>
|
||||
</resources>
|
||||
@@ -72,7 +72,6 @@
|
||||
<string name="always_on_vpn_support">Permitir VPN siempre-activada</string>
|
||||
<string name="location_services_not_detected">Servicios de Ubicación No Detectados</string>
|
||||
<string name="hint_search_packages">Buscar paquetes</string>
|
||||
<string name="clear_icon">Icono claro</string>
|
||||
<string name="attempt_connection">Intentando conexión...</string>
|
||||
<string name="vpn_starting">Iniciando VPN</string>
|
||||
<string name="db_name">wg-tunnel-db</string>
|
||||
@@ -106,7 +105,6 @@
|
||||
<string name="listen_port">Puerto de escucha</string>
|
||||
<string name="preshared_key">Clave previamente compartida</string>
|
||||
<string name="persistent_keepalive">Keepalive persistente</string>
|
||||
<string name="search_icon">Icono de búsqueda</string>
|
||||
<string name="docs_description">Leer documentación</string>
|
||||
<string name="email_description">Envíame un email</string>
|
||||
<string name="support_help_text">Si tienes problemas, ideas para mejoras, o simlemente comprometerte, tienes disponibles los siguientes recursos:</string>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="tunnels">Tunele</string>
|
||||
<string name="app_name">WG Tunnel</string>
|
||||
</resources>
|
||||
@@ -11,9 +11,9 @@
|
||||
<string name="icon">Иконка</string>
|
||||
<string name="turn_on">Включить</string>
|
||||
<string name="add_from_qr">Добавить из QR</string>
|
||||
<string name="qr_scan">Сканер QR</string>
|
||||
<string name="qr_scan">Сканировать QR</string>
|
||||
<string name="auto_tunneling">Авто-туннелирование</string>
|
||||
<string name="no_tunnels">Туннели еще не добавлены!</string>
|
||||
<string name="no_tunnels">Туннели ещё не добавлены!</string>
|
||||
<string name="open_file">Открыть файл</string>
|
||||
<string name="exclude">Исключить</string>
|
||||
<string name="include">Включить</string>
|
||||
@@ -23,10 +23,10 @@
|
||||
<string name="no_thanks">Нет, спасибо</string>
|
||||
<string name="map">Карта</string>
|
||||
<string name="addresses">Адреса</string>
|
||||
<string name="dns_servers">DNS сервера</string>
|
||||
<string name="allowed_ips">Разрешенные IP</string>
|
||||
<string name="dns_servers">DNS-серверы</string>
|
||||
<string name="allowed_ips">Разрешённые IP</string>
|
||||
<string name="endpoint">Конечная точка</string>
|
||||
<string name="restart">Перезагрузить туннель</string>
|
||||
<string name="restart">Перезапустить туннель</string>
|
||||
<string name="vpn_connection_failed">Ошибка соединения</string>
|
||||
<string name="always_on_vpn_support">Разрешить постоянный VPN</string>
|
||||
<string name="hint_search_packages">Поиск приложений</string>
|
||||
@@ -36,4 +36,136 @@
|
||||
<string name="interface_">Интерфейс</string>
|
||||
<string name="optional">(необязательно)</string>
|
||||
<string name="optional_no_recommend">(необязательно, не рекомендуется)</string>
|
||||
<string name="resume">Восстановить</string>
|
||||
<string name="tunneling_apps">Туннелируемые приложения</string>
|
||||
<string name="paused">приостановлено</string>
|
||||
<string name="active">активно</string>
|
||||
<string name="excluded">исключено</string>
|
||||
<string name="all">все</string>
|
||||
<string name="no_email_detected">Приложение для отправки почты не обнаружено</string>
|
||||
<string name="enter_pin">Введите свой PIN-код</string>
|
||||
<string name="enable_app_lock">Включить блокировку приложения</string>
|
||||
<string name="restart_on_ping">Перезапуск при сбое пинга (бета)</string>
|
||||
<string name="settings">Настройки</string>
|
||||
<string name="support">Поддержка</string>
|
||||
<string name="backend">Модуль</string>
|
||||
<string name="use_amnezia">"Использовать модуль Amnezia режима пользователя "</string>
|
||||
<string name="init_packet_junk_size">Начальный размер «мусорного» пакета</string>
|
||||
<string name="response_packet_junk_size">Размер ответного «мусорного» пакета</string>
|
||||
<string name="init_packet_magic_header">Заголовок пакета инициализации</string>
|
||||
<string name="see_the">Смотрите</string>
|
||||
<string name="tunnel_mobile_data">Туннелировать через мобильный интернет</string>
|
||||
<string name="tunnel_on_ethernet">Туннелировать через Ethernet</string>
|
||||
<string name="cancel">Отмена</string>
|
||||
<string name="docs_description">Посмотреть документацию</string>
|
||||
<string name="discord_description">Присоединиться к сообществу</string>
|
||||
<string name="email_chooser">Отправить письмо</string>
|
||||
<string name="add_trusted_ssid">Добавить доверенное имя сети Wi-Fi</string>
|
||||
<string name="included">включено</string>
|
||||
<string name="vpn_starting">Идёт запуск VPN</string>
|
||||
<string name="auto_tunnel_title">Сервис авто-туннелирования</string>
|
||||
<string name="create_import">Создать с нуля</string>
|
||||
<string name="private_key">Закрытый ключ</string>
|
||||
<string name="pause">Приостановить</string>
|
||||
<string name="logs_saved">Журнал сохранён в Загрузки</string>
|
||||
<string name="db_name">wg-tunnel-db</string>
|
||||
<string name="base64_key">Ключ в base64</string>
|
||||
<string name="error_no_file_explorer">Не найден файловый менеджер</string>
|
||||
<string name="delete_tunnel_message">Вы действительно хотите удалить этот туннель?</string>
|
||||
<string name="version">Версия</string>
|
||||
<string name="unknown_error">Неизвестная ошибка</string>
|
||||
<string name="tunnel_on_wifi">Туннель в недоверенных сетях Wi-Fi</string>
|
||||
<string name="error_file_extension">Файл не имеет расширение .conf или .zip</string>
|
||||
<string name="random">(случайно)</string>
|
||||
<string name="tunnel_start_text">Подключено к туннелю</string>
|
||||
<string name="app_name">WG Tunnel</string>
|
||||
<string name="vpn_channel_id">Канал VPN</string>
|
||||
<string name="vpn_channel_name">Канал уведомлений VPN</string>
|
||||
<string name="watcher_channel_id">Канал наблюдателя</string>
|
||||
<string name="watcher_channel_name">Канал уведомлений наблюдателя</string>
|
||||
<string name="watcher_notification_text_active">Отслеживание состояния сети: активно</string>
|
||||
<string name="notification_permission_required">Требуется разрешение на отображение уведомлений.</string>
|
||||
<string name="tunnels">Туннели</string>
|
||||
<string name="enable_auto_tunnel">Запустить авто-туннель</string>
|
||||
<string name="disable_auto_tunnel">Остановить авто-туннель</string>
|
||||
<string name="one_tunnel_required">Для использования этой функции нужно настроить хотя бы один туннель</string>
|
||||
<string name="okay">Хорошо</string>
|
||||
<string name="prominent_background_location_title">Фоновая передача местоположения</string>
|
||||
<string name="thank_you">Благодарим Вас за использование WG Tunnel!</string>
|
||||
<string name="trusted_ssid_value_description">Отправить SSID</string>
|
||||
<string name="trusted_ssid_empty_description">Введите SSID</string>
|
||||
<string name="add_tunnels_text">Добавить из файла или архива</string>
|
||||
<string name="location_services_not_detected">Сервисы местоположения не обнаружены</string>
|
||||
<string name="attempt_connection">Попытка соединения…</string>
|
||||
<string name="scanning_qr">Поиск QR-кода</string>
|
||||
<string name="none">Нет доверенных имён Wi-Fi</string>
|
||||
<string name="default_vpn_on">Основной VPN вкл.</string>
|
||||
<string name="default_vpn_off">Основной VPN выкл.</string>
|
||||
<string name="turn_off_auto">Необходимо приостановить или отключить авто-туннелирование</string>
|
||||
<string name="turn_on_tunnel">Действие требует наличие активного туннеля</string>
|
||||
<string name="add_peer">Добавить пира</string>
|
||||
<string name="rotate_keys">Обновить ключи</string>
|
||||
<string name="comma_separated_list">разделённый запятыми список</string>
|
||||
<string name="listen_port">Порт прослушивания</string>
|
||||
<string name="preshared_key">Общий ключ</string>
|
||||
<string name="seconds">секунд</string>
|
||||
<string name="persistent_keepalive">Поддержание работы туннеля (keepalive)</string>
|
||||
<string name="error_authentication_failed">Сбой аутентификации</string>
|
||||
<string name="export_configs">Экспорт конфигурации</string>
|
||||
<string name="location_services_required">Необходимо наличие служб местоположения</string>
|
||||
<string name="error_invalid_code">Некорректный QR-код</string>
|
||||
<string name="location_services_missing_message">Приложение не обнаружило сервис местоположения на вашем устройстве. На некоторых устройствах это может привести к невозможности определения имени сети Wi-Fi и сбою функции недоверенной сети. Всё равно продолжить?</string>
|
||||
<string name="error_ssid_exists">SSID уже существует</string>
|
||||
<string name="error_root_denied">Root-доступ запрещён</string>
|
||||
<string name="error_none">Нет ошибки</string>
|
||||
<string name="delete_tunnel">Удалить туннель</string>
|
||||
<string name="yes">Да</string>
|
||||
<string name="use_tunnel_on_wifi_name">Использовать туннель в сети Wi-Fi</string>
|
||||
<string name="disabled">отключено</string>
|
||||
<string name="auto_off">Приостановить авто-туннель</string>
|
||||
<string name="auto_tun_on">Восстановить авто-туннель</string>
|
||||
<string name="watcher_notification_text_paused">Отслеживание состояния сети: приостановлено</string>
|
||||
<string name="tunnel_start_title">VPN подключен</string>
|
||||
<string name="prominent_background_location_message">Эта функция требует фоновый доступ к местоположению для отслеживания имён сетей Wi-Fi, даже когда приложение закрыто. Для получения дополнительной информации, прочтите политику конфиденциальности на экране поддержки.</string>
|
||||
<string name="done">Готово</string>
|
||||
<string name="copy_public_key">Копировать открытый ключ</string>
|
||||
<string name="error_authorization_failed">Не удалось пройти аутентификацию</string>
|
||||
<string name="enabled_app_shortcuts">Включить ярлыки приложения</string>
|
||||
<string name="export_configs_failed">Не удалось экспортировать конфигурацию</string>
|
||||
<string name="precise_location_required">Необходимо точное местоположение</string>
|
||||
<string name="open_issue">Сообщить о проблеме</string>
|
||||
<string name="config_parse_error">Не удалось разобрать файл конфигурации</string>
|
||||
<string name="incorrect_pin">Некорректный PIN-код</string>
|
||||
<string name="pin_created">PIN создан успешно</string>
|
||||
<string name="create_pin">Создать PIN-код</string>
|
||||
<string name="mobile_data_tunnel">Назначить как туннель для мобильного интернета</string>
|
||||
<string name="edit_tunnel">Редактировать туннель</string>
|
||||
<string name="auto_on">Восстановить авто-туннель</string>
|
||||
<string name="auto_tun_off">Приостановить авто-туннель</string>
|
||||
<string name="junk_packet_count">Количество «мусорных» пакетов</string>
|
||||
<string name="junk_packet_maximum_size">Максимальный размер «мусорного» пакета</string>
|
||||
<string name="error_file_format">некорректный формат конфигурации туннеля</string>
|
||||
<string name="background_location_required">Необходим фоновый доступ к местоположению</string>
|
||||
<string name="exported_configs_message">Экспорт конфигурации в Загрузки</string>
|
||||
<string name="email_subject">Поддержка WG Tunnel</string>
|
||||
<string name="go">вперёд</string>
|
||||
<string name="email_description">Отправить письмо автору</string>
|
||||
<string name="support_help_text">Если у Вас возникли проблемы, или появилась идея по улучшению, или Вы просто хотите пообщаться, используйте следующие ресурсы:</string>
|
||||
<string name="use_kernel">Использовать модуль режима ядра</string>
|
||||
<string name="always_on_disabled">Функция всегда доступного VPN попыталась запустить туннель, но эта функция отключена в настройках.</string>
|
||||
<string name="no_browser_detected">Веб-браузер не обнаружен</string>
|
||||
<string name="read_logs">Посмотреть журнал</string>
|
||||
<string name="auto">(авто)</string>
|
||||
<string name="set_primary_tunnel">Назначить как главный туннель</string>
|
||||
<string name="no_wifi_names_configured">Имена сетей Wi-Fi не назначены этому туннелю</string>
|
||||
<string name="general">Общее</string>
|
||||
<string name="mode">Режим</string>
|
||||
<string name="userspace">Пользователя</string>
|
||||
<string name="kernel">Модуль ядра</string>
|
||||
<string name="junk_packet_minimum_size">Минимальный размер «мусорного» пакета</string>
|
||||
<string name="response_packet_magic_header">Заголовок пакета ответа</string>
|
||||
<string name="transport_packet_magic_header">Заголовок транспортного пакета</string>
|
||||
<string name="getting_started_guide">руководство по началу работы</string>
|
||||
<string name="unsure_how">, если не уверены, что делать дальше</string>
|
||||
<string name="underload_packet_magic_header">Заголовок пакета под нагрузкой</string>
|
||||
</resources>
|
||||
@@ -59,8 +59,6 @@
|
||||
<string name="always_on_vpn_support">Her Zaman Açık VPN\'e İzin Ver</string>
|
||||
<string name="location_services_not_detected">Konum Hizmetleri Algılanmadı</string>
|
||||
<string name="hint_search_packages">Uygulama arayın</string>
|
||||
<string name="clear_icon">Simgeyi Temizle</string>
|
||||
<string name="search_icon">Simge Ara</string>
|
||||
<string name="attempt_connection">Bağlantı kurulmaya çalışılıyor..</string>
|
||||
<string name="vpn_starting">VPN başlatılıyor</string>
|
||||
<string name="db_name">wg-tunnel-db</string>
|
||||
|
||||
@@ -58,8 +58,6 @@
|
||||
<string name="always_on_vpn_support">Allow Always-On VPN </string>
|
||||
<string name="location_services_not_detected">Location Services Not Detected</string>
|
||||
<string name="hint_search_packages">Search packages</string>
|
||||
<string name="clear_icon">Clear Icon</string>
|
||||
<string name="search_icon">Search Icon</string>
|
||||
<string name="attempt_connection">Attempting connection..</string>
|
||||
<string name="vpn_starting">VPN starting</string>
|
||||
<string name="db_name">wg-tunnel-db</string>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
object Constants {
|
||||
const val VERSION_NAME = "3.4.4"
|
||||
const val VERSION_NAME = "3.4.6"
|
||||
const val JVM_TARGET = "17"
|
||||
const val VERSION_CODE = 34400
|
||||
const val VERSION_CODE = 34600
|
||||
const val TARGET_SDK = 34
|
||||
const val MIN_SDK = 26
|
||||
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
||||
const val APP_NAME = "wgtunnel"
|
||||
const val COMPOSE_COMPILER_EXTENSION_VERSION = "1.5.11"
|
||||
const val COMPOSE_COMPILER_EXTENSION_VERSION = "1.5.14"
|
||||
|
||||
|
||||
const val STORE_PASS_VAR = "SIGNING_STORE_PASSWORD"
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
Funkce
|
||||
|
||||
- Přidání tunelů pomocí .conf souboru, zipu, ručního zadávání nebo QR kódu
|
||||
- Automatické připojení k VPN dle Wi-Fi SSID, ethernetu či mobilních dat
|
||||
- Rozdělené tunelování dle aplikací s vyhledáváním
|
||||
- WireGuard podpora pro kernel a userspace módy
|
||||
- Amnezia podpora pro userspace mód pro ochranu před cenzurou
|
||||
- Always-On VPN podpora
|
||||
- Exportování Amnezia a WireGuard tunelů do zipu
|
||||
- Podpora pro přepínání VPN pomocí rychlých dlaždic
|
||||
- Podpora statických zkratek u hlavního tunelu pro automatickou integraci
|
||||
- Podpora Intent automatizace pro všechny tunely
|
||||
- Automatický restart služby po restartu systému
|
||||
- Opatření pro úsporu baterie
|
||||
@@ -0,0 +1 @@
|
||||
Alternativní VPN klientská aplikace pro WireGuard s přidanými funkcemi
|
||||
@@ -0,0 +1 @@
|
||||
WG Tunnel
|
||||
@@ -0,0 +1,6 @@
|
||||
Was ist neu:
|
||||
- Offizielle Unterstützung für AmneziaWG
|
||||
- Import/Export von Amnezia Konfigurationen
|
||||
- Auto-Tunnel, um nur einmal pro Netzwerkänderung umzuschalten
|
||||
- Unterstützung für zusätzliche Sprachen
|
||||
- Weitere Fehlerbehebungen und Verbesserungen
|
||||
@@ -0,0 +1,5 @@
|
||||
Was ist neu:
|
||||
- Verbesserung der Benennung von Tunnel-Importen
|
||||
- Fehler des Anfangszustand beim automatischen Tunneln behoben
|
||||
- Verbesserte Fehlerhandhabung
|
||||
- Fehler beim Import von Amnezia zip behoben
|
||||
@@ -1,13 +1,14 @@
|
||||
Funktionen
|
||||
|
||||
- Hinzufügen von Tunneln über .conf Dateien, Zip, Manuelle Eingabe oder QR Codes
|
||||
- Automatische Verbindung zum VPN basierend auf der Wifi-SSID, Ethernet und mobilen Daten
|
||||
- Automatische Verbindung zum VPN basierend auf der WLAN-SSID, Ethernet und mobilen Daten
|
||||
- Geteilter Tunnel für Anwendungen mit Suche
|
||||
- Unterstützung für Wireguard Userspace- und Kernel-modus
|
||||
- Amnezia Unterstützung für Benutzeroberflächen-Modus zur DPI/Zensurschutz
|
||||
- Always-On VPN Unterstützung
|
||||
- Export von Tunneln ins Zip Format
|
||||
- Export von Amnezia- und WireGuard-Tunnel ins Zip Format
|
||||
- Quicktiles zum aktivieren/deaktivieren der VPN Verbindung
|
||||
- Feste Shortcuts für den Haupttunnel für automatische Integration
|
||||
- Intent basierten Automationssupport für alle Tunnel
|
||||
- Absichtlicher Automationssupport für alle Tunnel
|
||||
- Automatischer Servicestart nach Geräteneustart
|
||||
- Akkuerhaltungsfunktionen
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
What's new:
|
||||
- Fixes auto tunneling bugs
|
||||
- Fixes android backup bug
|
||||
- Bump versions
|
||||
@@ -0,0 +1,6 @@
|
||||
Funkcje
|
||||
|
||||
-Dodawanie tuneli za pomocą pliku .conf, zip, ręcznego wprowadzania lub kodu QR
|
||||
-Automatyczne łączenie się z VPN na podstawie SSID Wi-Fi, sieci Ethernet lub danych mobilnych
|
||||
-Dzielenie tuneli w aplikacji z wyszukiwaniem
|
||||
-
|
||||
@@ -0,0 +1 @@
|
||||
Alternatywna aplikacja klienta VPN dla WireGuard z dodatkowymi funkcjami
|
||||
@@ -0,0 +1 @@
|
||||
WG Tunel
|
||||
@@ -0,0 +1,3 @@
|
||||
Улучшения:
|
||||
- Исправлена ошибка с разрешениями на Android < 9
|
||||
- Другие оптимизации
|
||||
@@ -0,0 +1,5 @@
|
||||
Улучшения:
|
||||
- Добавлена статистики туннеля на главный экран
|
||||
- Улучшена навигация в экране настроек на Android TV
|
||||
- Удалена вибрация при оповещении
|
||||
- Другие исправления
|
||||
@@ -0,0 +1,5 @@
|
||||
Улучшения:
|
||||
- Добавлена поддержка авто-туннелирования только через мобильную сеть
|
||||
- Улучшен интерфейс экрана поддержки
|
||||
- Обновлены ссылки на ресурсы
|
||||
- Прочие исправления ошибок
|
||||
@@ -0,0 +1,5 @@
|
||||
Улучшения:
|
||||
- Базовая поддержка модуля WireGuard режима ядра
|
||||
- Улучшен процесс раскрытия информации о местоположении
|
||||
- Исправлена ошибка с разрешениями авто-туннеля
|
||||
- Прочие исправления интерфейса
|
||||
@@ -0,0 +1,2 @@
|
||||
Исправления:
|
||||
- Разрешения переднего плана в Android 14
|
||||
@@ -0,0 +1,7 @@
|
||||
Улучшения:
|
||||
- Рефакторинг хранения данных приложением
|
||||
- Улучшена навигация в интерфейсе для Android TV
|
||||
- Увеличена эффективность авто-туннелирования
|
||||
- Улучшена навигация
|
||||
- Возможность приостановить работу авто-туннеля
|
||||
- Множество исправлений ошибок
|
||||
@@ -0,0 +1,7 @@
|
||||
Улучшения:
|
||||
- Рефакторинг хранения данных приложением
|
||||
- Улучшена навигация в интерфейсе для Android TV
|
||||
- Увеличена эффективность авто-туннелирования
|
||||
- Улучшена навигация
|
||||
- Возможность приостановить работу авто-туннеля
|
||||
- Множество исправлений ошибок
|
||||
@@ -0,0 +1,8 @@
|
||||
Улучшения:
|
||||
- Рефакторинг хранения данных приложением
|
||||
- Улучшена навигация в интерфейсе для Android TV
|
||||
- Увеличена эффективность авто-туннелирования
|
||||
- Улучшена навигация
|
||||
- Возможность приостановить работу авто-туннеля
|
||||
- Исправлен запуск авто-туннеля на переднем плане
|
||||
- Множество исправлений ошибок
|
||||
@@ -0,0 +1,7 @@
|
||||
Улучшения:
|
||||
- Добавлено подтверждение при удалении туннеля
|
||||
- Добавлено разрешение на работу в фоне
|
||||
Исправления:
|
||||
- Зависание приложения при отключении туннеля
|
||||
- Ошибка в поле получателя электронной почты
|
||||
- Редактирование конфигурации с пустым полем DNS
|
||||
@@ -0,0 +1,3 @@
|
||||
Улучшения:
|
||||
- Исправлен сбой сохранения созданных настроек
|
||||
- Увеличен номер версии
|
||||
@@ -0,0 +1,2 @@
|
||||
Что нового:
|
||||
- Тестовая версия для отладки Continuous Integration
|
||||
@@ -0,0 +1,5 @@
|
||||
Что нового:
|
||||
- Автозапуск «Всегда включенного» VPN режима ядра при перезагрузке системы
|
||||
- Поддержка значков адаптивной темы
|
||||
- Исправлены значки уведомлений и плитки
|
||||
- Исправлены значки Android TV
|
||||
@@ -0,0 +1,5 @@
|
||||
Что нового:
|
||||
- Улучшен процесс первого запуска приложения
|
||||
- Переход на альтернативную реализацию библиотеки WireGuard
|
||||
- Запрос разрешения на доступ к VPN при первом подключении VPN
|
||||
- Увеличен номер версии
|
||||
@@ -0,0 +1,2 @@
|
||||
Что нового:
|
||||
- Исправлена ошибка в интерфейсе туннеля
|
||||
@@ -0,0 +1,4 @@
|
||||
Что нового:
|
||||
- Исправлена ошибка в интерфейсе редактора конфигурации
|
||||
- Добавлено уведомление AOVPN при первом запуске на Graphene OS
|
||||
- Увеличен номер версии
|
||||
@@ -0,0 +1,5 @@
|
||||
Что нового:
|
||||
- Добавлен экран с журналами выполнения
|
||||
- Добавлена блокировка приложения
|
||||
- Добавлен перезапуск туннеля при сбое пинга
|
||||
- Различные исправления ошибок
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user