feat: auto-tunneling flexibility

Added tunnel settings feature where users can configure a tunnel to be used on certain SSID or with mobile data.
Closes #50

Added feature where if a tunnel was active when phone restarted, the app will start that tunnel on boot.

Removed automatic auto-tunnel toggling/override from the tunnel tile and app shortcuts as it can cause undesirable behavior.

Added second tile to control auto-tunneling pause/resume state from a tile.

Added two additional static shortcuts to be able to control auto-tunneling pause/resume state from shortcuts.

Fixed bug where crashes can happen from serializing and deserializing tunnel configs by removing the need for serialization of tunnel configs.

Refactored logic of watcher and tunnel service to make state more predictable.
#127

Fixed bug where rapidly toggling tunnels can cause crashes.
Closes #145

Improved how tunnels are manually toggled from one to another.

Improved logic/storage around primary tunnel behavior.

Fixes issue where info level logs were not populating on release builds.

Increase allowed name length displayed in UI.
Closes #143

Fixes bug where androidTV could crash in certain situations.

Bump versions.

Updated screenshots.
This commit is contained in:
Zane Schepke 2024-03-29 23:53:41 -04:00
parent 1d644748e5
commit b1fdb5b9b2
103 changed files with 2323 additions and 1152 deletions

View File

@ -62,7 +62,8 @@ and on while on different networks. This app was created to offer a free solutio
## Docs (WIP)
Basic documentation of the feature and behaviors of this app can be found [here](https://zaneschepke.com/wgtunnel-docs/overview.html).
Basic documentation of the feature and behaviors of this app can be
found [here](https://zaneschepke.com/wgtunnel-docs/overview.html).
The repository for these docs can be found [here](https://github.com/zaneschepke/wgtunnel-docs).
@ -74,6 +75,7 @@ $ cd wgtunnel
```
And then build the app:
```
$ ./gradlew assembleDebug
```

View File

@ -0,0 +1,176 @@
{
"formatVersion": 1,
"database": {
"version": 7,
"identityHash": "e65e4e7cf01f50fb03196d47b54288b1",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelEnabled",
"columnName": "is_kernel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAutoTunnelPaused",
"columnName": "is_auto_tunnel_paused",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isMobileDataTunnel",
"columnName": "is_mobile_data_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e65e4e7cf01f50fb03196d47b54288b1')"
]
}
}

View File

@ -23,13 +23,13 @@ class MigrationTest {
@Test
@Throws(IOException::class)
fun migrate4To5() {
helper.createDatabase(dbName, 4).apply {
fun migrate6To7() {
helper.createDatabase(dbName, 6).apply {
// Database has schema version 1. Insert some data using SQL queries.
// You can't use DAO classes because they expect the latest schema.
execSQL(Queries.createDefaultSettings())
execSQL(
"INSERT INTO TunnelConfig (name, wg_quick)" + " VALUES ('hello', 'hello')",
Queries.createTunnelConfig(),
)
// Prepare for the next version.
close()
@ -37,7 +37,7 @@ class MigrationTest {
// Re-open the database with version 2 and provide
// MIGRATION_1_2 as the migration process.
helper.runMigrationsAndValidate(dbName, 5, true)
helper.runMigrationsAndValidate(dbName, 7, true)
// MigrationTestHelper automatically verifies the schema changes,
// but you need to validate that the data was migrated properly.
}

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
@ -101,7 +102,24 @@
android:name=".service.tile.TunnelControlTile"
android:exported="true"
android:icon="@drawable/ic_launcher"
android:label="WG Tunnel"
android:label="Tunnel control"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data
android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="true" />
<meta-data
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service
android:name=".service.tile.AutoTunnelControlTile"
android:exported="true"
android:icon="@drawable/ic_launcher"
android:label="Auto-tunnel"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data
android:name="android.service.quicksettings.ACTIVE_TILE"
@ -144,6 +162,7 @@
android:exported="false">
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />

View File

@ -5,6 +5,7 @@ import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.service.quicksettings.TileService
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
@ -19,6 +20,7 @@ class WireGuardAutoTunnel : Application() {
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) else Timber.plant(ReleaseTree())
PinManager.initialize(this)
}
companion object {
lateinit var instance: WireGuardAutoTunnel
private set
@ -27,11 +29,18 @@ class WireGuardAutoTunnel : Application() {
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
}
fun requestTileServiceStateUpdate(context : Context) {
fun requestTunnelTileServiceStateUpdate(context: Context) {
TileService.requestListeningState(
context,
ComponentName(instance, TunnelControlTile::class.java),
)
}
fun requestAutoTunnelTileServiceUpdate(context: Context) {
TileService.requestListeningState(
context,
ComponentName(instance, AutoTunnelControlTile::class.java),
)
}
}
}

View File

@ -2,31 +2,38 @@ package com.zaneschepke.wireguardautotunnel.data
import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.DeleteColumn
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.AutoMigrationSpec
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
@Database(
entities = [Settings::class, TunnelConfig::class],
version = 6,
version = 7,
autoMigrations =
[
AutoMigration(from = 1, to = 2),
AutoMigration(from = 2, to = 3),
AutoMigration(
from = 3,
to = 4,
),
AutoMigration(
from = 4,
to = 5,
),
AutoMigration(
from = 5,
to = 6,
),
],
[
AutoMigration(from = 1, to = 2),
AutoMigration(from = 2, to = 3),
AutoMigration(
from = 3,
to = 4,
),
AutoMigration(
from = 4,
to = 5,
),
AutoMigration(
from = 5,
to = 6,
),
AutoMigration(
from = 6,
to = 7,
spec = RemoveLegacySettingColumnsMigration::class,
),
],
exportSchema = true,
)
@TypeConverters(DatabaseListConverters::class)
@ -35,3 +42,13 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun tunnelConfigDoa(): TunnelConfigDao
}
@DeleteColumn(
tableName = "Settings",
columnName = "default_tunnel",
)
@DeleteColumn(
tableName = "Settings",
columnName = "is_battery_saver_enabled",
)
class RemoveLegacySettingColumnsMigration : AutoMigrationSpec

View File

@ -12,7 +12,7 @@ class DatabaseCallback : RoomDatabase.Callback() {
execSQL(Queries.createDefaultSettings())
Timber.i("Bootstrapping settings data")
setTransactionSuccessful()
} catch (e : Exception) {
} catch (e: Exception) {
Timber.e(e)
} finally {
endTransaction()

View File

@ -12,7 +12,7 @@ class DatabaseListConverters {
@TypeConverter
fun stringToList(value: String): MutableList<String> {
if (value.isEmpty()) return mutableListOf()
if (value.isBlank() || value.isEmpty()) return mutableListOf()
return try {
Json.decodeFromString<MutableList<String>>(value)
} catch (e: Exception) {

View File

@ -1,16 +1,14 @@
package com.zaneschepke.wireguardautotunnel.data
object Queries {
fun createDefaultSettings() : String {
fun createDefaultSettings(): String {
return """
INSERT INTO Settings (is_tunnel_enabled,
is_tunnel_on_mobile_data_enabled,
trusted_network_ssids,
default_tunnel,
is_always_on_vpn_enabled,
is_tunnel_on_ethernet_enabled,
is_shortcuts_enabled,
is_battery_saver_enabled,
is_tunnel_on_wifi_enabled,
is_kernel_enabled,
is_restore_on_boot_enabled,
@ -19,8 +17,6 @@ object Queries {
('false',
'false',
'sampleSSID1,sampleSSID2',
NULL,
'false',
'false',
'false',
'false',
@ -30,4 +26,10 @@ object Queries {
'false')
""".trimIndent()
}
fun createTunnelConfig(): String {
return """
INSERT INTO TunnelConfig (name, wg_quick) VALUES ('test', 'test')
""".trimIndent()
}
}

View File

@ -10,19 +10,27 @@ import kotlinx.coroutines.flow.Flow
@Dao
interface SettingsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: Settings)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun save(t: Settings)
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<Settings>)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun saveAll(t: List<Settings>)
@Query("SELECT * FROM settings WHERE id=:id") suspend fun getById(id: Long): Settings?
@Query("SELECT * FROM settings WHERE id=:id")
suspend fun getById(id: Long): Settings?
@Query("SELECT * FROM settings") suspend fun getAll(): List<Settings>
@Query("SELECT * FROM settings")
suspend fun getAll(): List<Settings>
@Query("SELECT * FROM settings LIMIT 1") fun getSettingsFlow(): Flow<Settings>
@Query("SELECT * FROM settings LIMIT 1")
fun getSettingsFlow(): Flow<Settings>
@Query("SELECT * FROM settings") fun getAllFlow(): Flow<MutableList<Settings>>
@Query("SELECT * FROM settings")
fun getAllFlow(): Flow<MutableList<Settings>>
@Delete suspend fun delete(t: Settings)
@Delete
suspend fun delete(t: Settings)
@Query("SELECT COUNT('id') FROM settings") suspend fun count(): Long
@Query("SELECT COUNT('id') FROM settings")
suspend fun count(): Long
}

View File

@ -6,21 +6,44 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
import kotlinx.coroutines.flow.Flow
@Dao
interface TunnelConfigDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: TunnelConfig)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun save(t: TunnelConfig)
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<TunnelConfig>)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun saveAll(t: TunnelConfigs)
@Query("SELECT * FROM TunnelConfig WHERE id=:id") suspend fun getById(id: Long): TunnelConfig?
@Query("SELECT * FROM TunnelConfig WHERE id=:id")
suspend fun getById(id: Long): TunnelConfig?
@Query("SELECT * FROM TunnelConfig") suspend fun getAll(): List<TunnelConfig>
@Query("SELECT * FROM TunnelConfig")
suspend fun getAll(): TunnelConfigs
@Delete suspend fun delete(t: TunnelConfig)
@Delete
suspend fun delete(t: TunnelConfig)
@Query("SELECT COUNT('id') FROM TunnelConfig") suspend fun count(): Long
@Query("SELECT COUNT('id') FROM TunnelConfig")
suspend fun count(): Long
@Query("SELECT * FROM tunnelconfig") fun getAllFlow(): Flow<MutableList<TunnelConfig>>
@Query("SELECT * FROM TunnelConfig WHERE tunnel_networks LIKE '%' || :name || '%'")
suspend fun findByTunnelNetworkName(name: String): TunnelConfigs
@Query("UPDATE TunnelConfig SET is_primary_tunnel = 0 WHERE is_primary_tunnel =1")
fun resetPrimaryTunnel()
@Query("UPDATE TunnelConfig SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1")
fun resetMobileDataTunnel()
@Query("SELECT * FROM TUNNELCONFIG WHERE is_primary_tunnel=1")
suspend fun findByPrimary(): TunnelConfigs
@Query("SELECT * FROM TUNNELCONFIG WHERE is_mobile_data_tunnel=1")
suspend fun findByMobileDataTunnel(): TunnelConfigs
@Query("SELECT * FROM tunnelconfig")
fun getAllFlow(): Flow<MutableList<TunnelConfig>>
}

View File

@ -4,39 +4,66 @@ import android.content.Context
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import timber.log.Timber
import java.io.IOException
class DataStoreManager(private val context: Context) {
companion object {
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
val TUNNEL_RUNNING_FROM_MANUAL_START =
booleanPreferencesKey("TUNNEL_RUNNING_FROM_MANUAL_START")
val ACTIVE_TUNNEL = intPreferencesKey("ACTIVE_TUNNEL")
val CURRENT_SSID = stringPreferencesKey("CURRENT_SSID")
}
// preferences
private val preferencesKey = "preferences"
private val Context.dataStore by
preferencesDataStore(
name = preferencesKey,
)
preferencesDataStore(
name = preferencesKey,
)
suspend fun init() {
context.dataStore.data.first()
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)
}
}
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) =
context.dataStore.edit { it[key] = value }
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] }
suspend fun <T> getFromStore(key: Preferences.Key<T>) =
context.dataStore.data.map{ it[key] }.first()
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
}
}
fun <T> getFromStoreBlocking(key: Preferences.Key<T>) = runBlocking {
context.dataStore.data.map{ it[key] }.first()
context.dataStore.data.map { it[key] }.first()
}
val preferencesFlow: Flow<Preferences?> = context.dataStore.data

View File

@ -0,0 +1,14 @@
package com.zaneschepke.wireguardautotunnel.data.model
data class GeneralState(
val locationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT,
val batteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
val tunnelRunningFromManualStart: Boolean = TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
val activeTunnelId: Int? = null
) {
companion object {
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
const val TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT = false
}
}

View File

@ -7,62 +7,47 @@ import androidx.room.PrimaryKey
@Entity
data class Settings(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "is_tunnel_enabled") var isAutoTunnelEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_enabled") val isAutoTunnelEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled")
var isTunnelOnMobileDataEnabled: Boolean = false,
val isTunnelOnMobileDataEnabled: Boolean = false,
@ColumnInfo(name = "trusted_network_ssids")
var trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
@ColumnInfo(name = "default_tunnel") var defaultTunnel: String? = null,
@ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled: Boolean = false,
val trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
@ColumnInfo(name = "is_always_on_vpn_enabled") val isAlwaysOnVpnEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled")
var isTunnelOnEthernetEnabled: Boolean = false,
val isTunnelOnEthernetEnabled: Boolean = false,
@ColumnInfo(
name = "is_shortcuts_enabled",
defaultValue = "false",
)
var isShortcutsEnabled: Boolean = false,
@ColumnInfo(
name = "is_battery_saver_enabled",
defaultValue = "false",
)
var isBatterySaverEnabled: Boolean = false,
val isShortcutsEnabled: Boolean = false,
@ColumnInfo(
name = "is_tunnel_on_wifi_enabled",
defaultValue = "false",
)
var isTunnelOnWifiEnabled: Boolean = false,
val isTunnelOnWifiEnabled: Boolean = false,
@ColumnInfo(
name = "is_kernel_enabled",
defaultValue = "false",
)
var isKernelEnabled: Boolean = false,
val isKernelEnabled: Boolean = false,
@ColumnInfo(
name = "is_restore_on_boot_enabled",
defaultValue = "false",
)
var isRestoreOnBootEnabled: Boolean = false,
val isRestoreOnBootEnabled: Boolean = false,
@ColumnInfo(
name = "is_multi_tunnel_enabled",
defaultValue = "false",
)
var isMultiTunnelEnabled: Boolean = false,
val isMultiTunnelEnabled: Boolean = false,
@ColumnInfo(
name = "is_auto_tunnel_paused",
defaultValue = "false",
)
var isAutoTunnelPaused: Boolean = false,
val isAutoTunnelPaused: Boolean = false,
@ColumnInfo(
name = "is_ping_enabled",
defaultValue = "false",
)
var isPingEnabled: Boolean = false,
) {
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean {
return if (defaultTunnel != null) {
val defaultConfig = TunnelConfig.from(defaultTunnel!!)
(tunnelConfig.id == defaultConfig.id)
} else {
false
}
}
}
val isPingEnabled: Boolean = false,
)

View File

@ -5,26 +5,30 @@ import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import com.wireguard.config.Config
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.io.InputStream
@Entity(indices = [Index(value = ["name"], unique = true)])
@Serializable
data class TunnelConfig(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "name") var name: String,
@ColumnInfo(name = "wg_quick") var wgQuick: String
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "wg_quick") val wgQuick: String,
@ColumnInfo(
name = "tunnel_networks",
defaultValue = "",
)
val tunnelNetworks: MutableList<String> = mutableListOf(),
@ColumnInfo(
name = "is_mobile_data_tunnel",
defaultValue = "false",
)
val isMobileDataTunnel: Boolean = false,
@ColumnInfo(
name = "is_primary_tunnel",
defaultValue = "false",
)
val isPrimaryTunnel: Boolean = false,
) {
override fun toString(): String {
return Json.encodeToString(serializer(), this)
}
companion object {
fun from(string: String): TunnelConfig {
return Json.decodeFromString<TunnelConfig>(string)
}
fun configFromQuick(wgQuick: String): Config {
val inputStream: InputStream = wgQuick.byteInputStream()
val reader = inputStream.bufferedReader(Charsets.UTF_8)

View File

@ -0,0 +1,14 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
interface AppDataRepository {
suspend fun getPrimaryOrFirstTunnel(): TunnelConfig?
suspend fun getStartTunnelConfig(): TunnelConfig?
suspend fun toggleWatcherServicePause()
val settings: SettingsRepository
val tunnels: TunnelConfigRepository
val appState: AppStateRepository
}

View File

@ -0,0 +1,34 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import javax.inject.Inject
class AppDataRoomRepository @Inject constructor(
override val settings: SettingsRepository,
override val tunnels: TunnelConfigRepository,
override val appState: AppStateRepository
) : AppDataRepository {
override suspend fun getPrimaryOrFirstTunnel(): TunnelConfig? {
return tunnels.findPrimary().firstOrNull() ?: tunnels.getAll().firstOrNull()
}
override suspend fun getStartTunnelConfig(): TunnelConfig? {
return if (appState.isTunnelRunningFromManualStart()) {
appState.getActiveTunnelId()?.let {
tunnels.getById(it)
}
} else null
}
override suspend fun toggleWatcherServicePause() {
val settings = settings.getSettings()
if (settings.isAutoTunnelEnabled) {
val pauseAutoTunnel = !settings.isAutoTunnelPaused
this.settings.save(
settings.copy(
isAutoTunnelPaused = pauseAutoTunnel,
),
)
}
}
}

View File

@ -0,0 +1,26 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
import kotlinx.coroutines.flow.Flow
interface AppStateRepository {
suspend fun isLocationDisclosureShown(): Boolean
suspend fun setLocationDisclosureShown(shown: Boolean)
suspend fun isBatteryOptimizationDisableShown(): Boolean
suspend fun setBatteryOptimizationDisableShown(shown: Boolean)
suspend fun isTunnelRunningFromManualStart(): Boolean
suspend fun setTunnelRunningFromManualStart(id: Int)
suspend fun setManualStop()
suspend fun getActiveTunnelId(): Int?
suspend fun getCurrentSsid(): String?
suspend fun setCurrentSsid(ssid: String)
val generalStateFlow: Flow<GeneralState>
}

View File

@ -0,0 +1,81 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import timber.log.Timber
class DataStoreAppStateRepository(private val dataStoreManager: DataStoreManager) :
AppStateRepository {
override suspend fun isLocationDisclosureShown(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN)
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT
}
override suspend fun setLocationDisclosureShown(shown: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, shown)
}
override suspend fun isBatteryOptimizationDisableShown(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN)
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT
}
override suspend fun setBatteryOptimizationDisableShown(shown: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, shown)
}
override suspend fun isTunnelRunningFromManualStart(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START)
?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT
}
override suspend fun setTunnelRunningFromManualStart(id: Int) {
setTunnelRunningFromManualStart(true)
setActiveTunnelId(id)
}
override suspend fun setManualStop() {
setTunnelRunningFromManualStart(false)
}
private suspend fun setTunnelRunningFromManualStart(running: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START, running)
}
override suspend fun getActiveTunnelId(): Int? {
return dataStoreManager.getFromStore(DataStoreManager.ACTIVE_TUNNEL)
}
private suspend fun setActiveTunnelId(id: Int) {
dataStoreManager.saveToDataStore(DataStoreManager.ACTIVE_TUNNEL, id)
}
override suspend fun getCurrentSsid(): String? {
return dataStoreManager.getFromStore(DataStoreManager.CURRENT_SSID)
}
override suspend fun setCurrentSsid(ssid: String) {
dataStoreManager.saveToDataStore(DataStoreManager.CURRENT_SSID, ssid)
}
override val generalStateFlow: Flow<GeneralState> =
dataStoreManager.preferencesFlow.map { prefs ->
prefs?.let { pref ->
try {
GeneralState(
locationDisclosureShown = pref[DataStoreManager.LOCATION_DISCLOSURE_SHOWN]
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT,
batteryOptimizationDisableShown = pref[DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN]
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
tunnelRunningFromManualStart = pref[DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START]
?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
)
} catch (e: IllegalArgumentException) {
Timber.e(e)
GeneralState()
}
} ?: GeneralState()
}
}

View File

@ -4,7 +4,7 @@ import com.zaneschepke.wireguardautotunnel.data.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import kotlinx.coroutines.flow.Flow
class SettingsRepositoryImpl(private val settingsDoa: SettingsDao) : SettingsRepository {
class RoomSettingsRepository(private val settingsDoa: SettingsDao) : SettingsRepository {
override suspend fun save(settings: Settings) {
settingsDoa.save(settings)

View File

@ -0,0 +1,68 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
import kotlinx.coroutines.flow.Flow
class RoomTunnelConfigRepository(private val tunnelConfigDao: TunnelConfigDao) :
TunnelConfigRepository {
override fun getTunnelConfigsFlow(): Flow<TunnelConfigs> {
return tunnelConfigDao.getAllFlow()
}
override suspend fun getAll(): TunnelConfigs {
return tunnelConfigDao.getAll()
}
override suspend fun save(tunnelConfig: TunnelConfig) {
tunnelConfigDao.save(tunnelConfig)
}
override suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?) {
tunnelConfigDao.resetPrimaryTunnel()
tunnelConfig?.let {
save(
it.copy(
isPrimaryTunnel = true,
),
)
}
}
override suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?) {
tunnelConfigDao.resetMobileDataTunnel()
tunnelConfig?.let {
save(
it.copy(
isMobileDataTunnel = true,
),
)
}
}
override suspend fun delete(tunnelConfig: TunnelConfig) {
tunnelConfigDao.delete(tunnelConfig)
}
override suspend fun getById(id: Int): TunnelConfig? {
return tunnelConfigDao.getById(id.toLong())
}
override suspend fun count(): Int {
return tunnelConfigDao.count().toInt()
}
override suspend fun findByTunnelNetworksName(name: String): TunnelConfigs {
return tunnelConfigDao.findByTunnelNetworkName(name)
}
override suspend fun findByMobileDataTunnel(): TunnelConfigs {
return tunnelConfigDao.findByMobileDataTunnel()
}
override suspend fun findPrimary(): TunnelConfigs {
return tunnelConfigDao.findByPrimary()
}
}

View File

@ -12,7 +12,19 @@ interface TunnelConfigRepository {
suspend fun save(tunnelConfig: TunnelConfig)
suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?)
suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?)
suspend fun delete(tunnelConfig: TunnelConfig)
suspend fun getById(id: Int): TunnelConfig?
suspend fun count(): Int
suspend fun findByTunnelNetworksName(name: String): TunnelConfigs
suspend fun findByMobileDataTunnel(): TunnelConfigs
suspend fun findPrimary(): TunnelConfigs
}

View File

@ -1,29 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
import kotlinx.coroutines.flow.Flow
class TunnelConfigRepositoryImpl(private val tunnelConfigDao: TunnelConfigDao) :
TunnelConfigRepository {
override fun getTunnelConfigsFlow(): Flow<TunnelConfigs> {
return tunnelConfigDao.getAllFlow()
}
override suspend fun getAll(): TunnelConfigs {
return tunnelConfigDao.getAll()
}
override suspend fun save(tunnelConfig: TunnelConfig) {
tunnelConfigDao.save(tunnelConfig)
}
override suspend fun delete(tunnelConfig: TunnelConfig) {
tunnelConfigDao.delete(tunnelConfig)
}
override suspend fun count(): Int {
return tunnelConfigDao.count().toInt()
}
}

View File

@ -19,10 +19,10 @@ class DatabaseModule {
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
context.getString(R.string.db_name),
)
context,
AppDatabase::class.java,
context.getString(R.string.db_name),
)
.fallbackToDestructiveMigration()
.addCallback(DatabaseCallback())
.build()

View File

@ -2,4 +2,6 @@ package com.zaneschepke.wireguardautotunnel.module
import javax.inject.Qualifier
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Kernel
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Kernel

View File

@ -5,10 +5,14 @@ import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRoomRepository
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.data.repository.DataStoreAppStateRepository
import com.zaneschepke.wireguardautotunnel.data.repository.RoomSettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.RoomTunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepositoryImpl
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepositoryImpl
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -34,13 +38,13 @@ class RepositoryModule {
@Singleton
@Provides
fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao): TunnelConfigRepository {
return TunnelConfigRepositoryImpl(tunnelConfigDao)
return RoomTunnelConfigRepository(tunnelConfigDao)
}
@Singleton
@Provides
fun provideSettingsRepository(settingsDao: SettingsDao): SettingsRepository {
return SettingsRepositoryImpl(settingsDao)
return RoomSettingsRepository(settingsDao)
}
@Singleton
@ -48,4 +52,22 @@ class RepositoryModule {
fun providePreferencesDataStore(@ApplicationContext context: Context): DataStoreManager {
return DataStoreManager(context)
}
@Provides
@Singleton
fun provideGeneralStateRepository(dataStoreManager: DataStoreManager): AppStateRepository {
return DataStoreAppStateRepository(dataStoreManager)
}
@Provides
@Singleton
fun provideAppDataRepository(
settingsRepository: SettingsRepository,
tunnelConfigRepository: TunnelConfigRepository,
appStateRepository: AppStateRepository
): AppDataRepository {
return AppDataRoomRepository(settingsRepository, tunnelConfigRepository, appStateRepository)
}
}

View File

@ -6,7 +6,8 @@ import com.wireguard.android.backend.GoBackend
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell
import com.wireguard.android.util.ToolsInstaller
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
import dagger.Module
@ -44,8 +45,14 @@ class TunnelModule {
fun provideVpnService(
@Userspace userspaceBackend: Backend,
@Kernel kernelBackend: Backend,
settingsRepository: SettingsRepository
appDataRepository: AppDataRepository
): VpnService {
return WireGuardTunnel(userspaceBackend, kernelBackend, settingsRepository)
return WireGuardTunnel(userspaceBackend, kernelBackend, appDataRepository)
}
@Provides
@Singleton
fun provideServiceManager(appDataRepository: AppDataRepository): ServiceManager {
return ServiceManager(appDataRepository)
}
}

View File

@ -2,4 +2,6 @@ package com.zaneschepke.wireguardautotunnel.module
import javax.inject.Qualifier
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Userspace
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Userspace

View File

@ -3,8 +3,7 @@ package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.util.goAsync
import dagger.hilt.android.AndroidEntryPoint
@ -14,19 +13,33 @@ import javax.inject.Inject
@AndroidEntryPoint
class BootReceiver : BroadcastReceiver() {
@Inject lateinit var settingsRepository: SettingsRepository
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
@Inject
lateinit var serviceManager: ServiceManager
override fun onReceive(context: Context?, intent: Intent?) = goAsync {
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
val settings = settingsRepository.getSettings()
if (settings.isAutoTunnelEnabled) {
Timber.i("Starting watcher service from boot")
ServiceManager.startWatcherServiceForeground(context!!)
} else if(settings.isAlwaysOnVpnEnabled) {
Timber.i("Starting tunnel from boot")
ServiceManager.startVpnServicePrimaryTunnel(context!!, settings, tunnelConfigRepository.getAll().firstOrNull())
context?.run {
val settings = appDataRepository.settings.getSettings()
if (settings.isAutoTunnelEnabled) {
Timber.i("Starting watcher service from boot")
serviceManager.startWatcherServiceForeground(context)
}
if (appDataRepository.appState.isTunnelRunningFromManualStart()) {
appDataRepository.appState.getActiveTunnelId()?.let {
Timber.i("Starting tunnel that was active before reboot")
serviceManager.startVpnServiceForeground(
context,
appDataRepository.tunnels.getById(it)?.id,
)
}
}
if (settings.isAlwaysOnVpnEnabled) {
Timber.i("Starting vpn service from boot AOVPN")
serviceManager.startVpnServiceForeground(context)
}
}
}
}

View File

@ -10,20 +10,25 @@ import com.zaneschepke.wireguardautotunnel.util.goAsync
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class NotificationActionReceiver : BroadcastReceiver() {
@Inject lateinit var settingsRepository: SettingsRepository
@Inject
lateinit var settingsRepository: SettingsRepository
@Inject
lateinit var serviceManager: ServiceManager
override fun onReceive(context: Context, intent: Intent?) = goAsync {
try {
val settings = settingsRepository.getSettings()
if (settings.defaultTunnel != null) {
ServiceManager.stopVpnService(context)
delay(Constants.TOGGLE_TUNNEL_DELAY)
ServiceManager.startVpnServiceForeground(context, settings.defaultTunnel.toString())
}
//TODO fix for manual start changes when enabled
serviceManager.stopVpnService(context)
delay(Constants.TOGGLE_TUNNEL_DELAY)
serviceManager.startVpnServiceForeground(context)
} catch (e: Exception) {
Timber.e(e)
} finally {
cancel()
}

View File

@ -4,6 +4,7 @@ import android.content.Intent
import android.os.Bundle
import android.os.IBinder
import androidx.lifecycle.LifecycleService
import com.zaneschepke.wireguardautotunnel.util.Constants
import timber.log.Timber
open class ForegroundService : LifecycleService() {
@ -23,11 +24,13 @@ open class ForegroundService : LifecycleService() {
when (action) {
Action.START.name,
Action.START_FOREGROUND.name -> startService(intent.extras)
Action.STOP.name -> stopService(intent.extras)
"android.net.VpnService" -> {
Timber.d("Always-on VPN starting service")
Constants.ALWAYS_ON_VPN_ACTION -> {
Timber.i("Always-on VPN starting service")
startService(intent.extras)
}
else -> Timber.d("This should never happen. No action in the received intent")
}
} else {

View File

@ -3,33 +3,17 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.Service
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.Constants
import timber.log.Timber
object ServiceManager {
// private
// fun <T> Context.isServiceRunning(service: Class<T>) =
// (getSystemService(ACTIVITY_SERVICE) as ActivityManager)
// .runningAppProcesses.any {
// it.processName == service.name
// }
//
// fun <T : Service> getServiceState(
// context: Context,
// cls: Class<T>
// ): ServiceState {
// val isServiceRunning = context.isServiceRunning(cls)
// return if (isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED
// }
class ServiceManager(private val appDataRepository: AppDataRepository) {
private fun <T : Service> actionOnService(
action: Action,
context: Context,
cls: Class<T>,
extras: Map<String, String>? = null
extras: Map<String, Int>? = null
) {
val intent =
Intent(context, cls).also {
@ -42,9 +26,11 @@ object ServiceManager {
Action.START_FOREGROUND -> {
context.startForegroundService(intent)
}
Action.START -> {
context.startService(intent)
}
Action.STOP -> context.startService(intent)
}
} catch (e: Exception) {
@ -52,16 +38,22 @@ object ServiceManager {
}
}
fun startVpnService(context: Context, tunnelConfig: String) {
suspend fun startVpnService(
context: Context,
tunnelId: Int? = null,
isManualStart: Boolean = false
) {
if (isManualStart) onManualStart(tunnelId)
actionOnService(
Action.START,
context,
WireGuardTunnelService::class.java,
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig),
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
)
}
fun stopVpnService(context: Context) {
suspend fun stopVpnService(context: Context, isManualStop: Boolean = false) {
if (isManualStop) onManualStop()
Timber.d("Stopping vpn service action")
actionOnService(
Action.STOP,
@ -70,24 +62,30 @@ object ServiceManager {
)
}
fun startVpnServiceForeground(context: Context, tunnelConfig: String) {
private suspend fun onManualStop() {
appDataRepository.appState.setManualStop()
}
private suspend fun onManualStart(tunnelId: Int?) {
tunnelId?.let {
appDataRepository.appState.setTunnelRunningFromManualStart(it)
}
}
suspend fun startVpnServiceForeground(
context: Context,
tunnelId: Int? = null,
isManualStart: Boolean = false
) {
if (isManualStart) onManualStart(tunnelId)
actionOnService(
Action.START_FOREGROUND,
context,
WireGuardTunnelService::class.java,
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig),
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
)
}
fun startVpnServicePrimaryTunnel(context: Context, settings: Settings, fallbackTunnel: TunnelConfig? = null) {
if(settings.defaultTunnel != null) {
return startVpnServiceForeground(context, settings.defaultTunnel!!)
}
if(fallbackTunnel != null) {
startVpnServiceForeground(context, fallbackTunnel.toString())
}
}
fun startWatcherServiceForeground(
context: Context,
) {

View File

@ -0,0 +1,84 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
data class WatcherState(
val isWifiConnected: Boolean = false,
val config: TunnelConfig? = null,
val vpnStatus: Tunnel.State = Tunnel.State.DOWN,
val isEthernetConnected: Boolean = false,
val isMobileDataConnected: Boolean = false,
val currentNetworkSSID: String = "",
val settings: Settings = Settings()
) {
private fun isVpnConnected() = vpnStatus == Tunnel.State.UP
fun isEthernetConditionMet(): Boolean {
return (isEthernetConnected &&
settings.isTunnelOnEthernetEnabled &&
!isVpnConnected())
}
fun isMobileDataConditionMet(): Boolean {
return (!isEthernetConnected &&
settings.isTunnelOnMobileDataEnabled &&
!isWifiConnected &&
isMobileDataConnected &&
!isVpnConnected())
}
fun isTunnelNotMobileDataPreferredConditionMet(): Boolean {
return (!isEthernetConnected &&
settings.isTunnelOnMobileDataEnabled &&
!isWifiConnected &&
isMobileDataConnected &&
config?.isMobileDataTunnel == false && isVpnConnected())
}
fun isTunnelOffOnMobileDataConditionMet(): Boolean {
return (!isEthernetConnected &&
!settings.isTunnelOnMobileDataEnabled &&
isMobileDataConnected &&
!isWifiConnected &&
isVpnConnected())
}
fun isUntrustedWifiConditionMet(): Boolean {
return (!isEthernetConnected &&
isWifiConnected &&
!settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
settings.isTunnelOnWifiEnabled
&& !isVpnConnected())
}
fun isTunnelNotWifiNamePreferredMet(ssid: String): Boolean {
return (!isEthernetConnected &&
isWifiConnected &&
!settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
settings.isTunnelOnWifiEnabled && config?.tunnelNetworks?.contains(ssid) == false && isVpnConnected())
}
fun isTrustedWifiConditionMet(): Boolean {
return (!isEthernetConnected &&
(isWifiConnected &&
settings.trustedNetworkSSIDs.contains(currentNetworkSSID)) &&
(isVpnConnected()))
}
fun isTunnelOffOnWifiConditionMet(): Boolean {
return (!isEthernetConnected &&
(isWifiConnected &&
!settings.isTunnelOnWifiEnabled &&
(isVpnConnected())))
}
fun isTunnelOffOnNoConnectivityMet(): Boolean {
return (!isEthernetConnected &&
!isWifiConnected &&
!isMobileDataConnected &&
(isVpnConnected()))
}
}

View File

@ -7,8 +7,8 @@ import androidx.core.app.ServiceCompat
import androidx.lifecycle.lifecycleScope
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
@ -33,29 +33,29 @@ import javax.inject.Inject
class WireGuardConnectivityWatcherService : ForegroundService() {
private val foregroundId = 122
@Inject lateinit var wifiService: NetworkService<WifiService>
@Inject
lateinit var wifiService: NetworkService<WifiService>
@Inject lateinit var mobileDataService: NetworkService<MobileDataService>
@Inject
lateinit var mobileDataService: NetworkService<MobileDataService>
@Inject lateinit var ethernetService: NetworkService<EthernetService>
@Inject
lateinit var ethernetService: NetworkService<EthernetService>
@Inject lateinit var settingsRepository: SettingsRepository
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject lateinit var notificationService: NotificationService
@Inject
lateinit var notificationService: NotificationService
@Inject lateinit var vpnService: VpnService
@Inject
lateinit var vpnService: VpnService
@Inject
lateinit var serviceManager: ServiceManager
private val networkEventsFlow = MutableStateFlow(WatcherState())
data class WatcherState(
val isWifiConnected: Boolean = false,
val isVpnConnected: Boolean = false,
val isEthernetConnected: Boolean = false,
val isMobileDataConnected: Boolean = false,
val currentNetworkSSID: String = "",
val settings: Settings = Settings()
)
private lateinit var watcherJob: Job
private var wakeLock: PowerManager.WakeLock? = null
@ -65,7 +65,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
super.onCreate()
lifecycleScope.launch(Dispatchers.Main) {
try {
if (settingsRepository.getSettings().isAutoTunnelPaused) {
if (appDataRepository.settings.getSettings().isAutoTunnelPaused) {
launchWatcherPausedNotification()
} else launchWatcherNotification()
} catch (e: Exception) {
@ -121,16 +121,16 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
private fun initWakeLock() {
wakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
try {
Timber.i("Initiating wakelock with 10 min timeout")
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
} finally {
release()
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
try {
Timber.i("Initiating wakelock with 10 min timeout")
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
} finally {
release()
}
}
}
}
}
private fun cancelWatcherJob() {
@ -142,7 +142,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
private fun startWatcherJob() {
watcherJob =
lifecycleScope.launch(Dispatchers.IO) {
val setting = settingsRepository.getSettings()
val setting = appDataRepository.settings.getSettings()
launch {
Timber.i("Starting wifi watcher")
watchForWifiConnectivityChanges()
@ -167,7 +167,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
Timber.i("Starting settings watcher")
watchForSettingsChanges()
}
if(setting.isPingEnabled) {
if (setting.isPingEnabled) {
launch {
Timber.i("Starting ping watcher")
watchForPingFailure()
@ -191,6 +191,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
isMobileDataConnected = true,
)
}
is NetworkStatus.CapabilitiesChanged -> {
networkEventsFlow.value =
networkEventsFlow.value.copy(
@ -198,6 +199,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
)
Timber.i("Mobile data capabilities changed")
}
is NetworkStatus.Unavailable -> {
networkEventsFlow.value =
networkEventsFlow.value.copy(
@ -212,39 +214,39 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
private suspend fun watchForPingFailure() {
try {
do {
if(vpnService.vpnState.value.status == Tunnel.State.UP) {
val config = vpnService.vpnState.value.config
config?.let {
val results = it.peers.map { peer ->
val host = if(peer.endpoint.isPresent &&
if (vpnService.vpnState.value.status == Tunnel.State.UP) {
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
tunnelConfig?.let {
val config = TunnelConfig.configFromQuick(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.BACKUP_PING_HOST
Timber.i("Checking reachability of: $host")
val reachable = InetAddress.getByName(host).isReachable(Constants.PING_TIMEOUT.toInt())
val reachable = InetAddress.getByName(host)
.isReachable(Constants.PING_TIMEOUT.toInt())
Timber.i("Result: reachable - $reachable")
reachable
}
if(results.contains(false)) {
if (results.contains(false)) {
Timber.i("Restarting VPN for ping failure")
ServiceManager.stopVpnService(this)
serviceManager.stopVpnService(this)
delay(Constants.VPN_RESTART_DELAY)
val tunnel = networkEventsFlow.value.settings.defaultTunnel
ServiceManager.startVpnServiceForeground(this, tunnel!!)
serviceManager.startVpnServiceForeground(this)
delay(Constants.PING_COOLDOWN)
}
}
}
delay(Constants.PING_INTERVAL)
} while (true)
} catch (e: Exception) {
Timber.e(e)
}
}
private suspend fun watchForSettingsChanges() {
settingsRepository.getSettingsFlow().collect {
appDataRepository.settings.getSettingsFlow().collect {
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
when (it.isAutoTunnelPaused) {
true -> launchWatcherPausedNotification()
@ -260,19 +262,11 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
private suspend fun watchForVpnConnectivityChanges() {
vpnService.vpnState.collect {
when (it.status) {
Tunnel.State.DOWN ->
networkEventsFlow.value =
networkEventsFlow.value.copy(
isVpnConnected = false,
)
Tunnel.State.UP ->
networkEventsFlow.value =
networkEventsFlow.value.copy(
isVpnConnected = true,
)
else -> {}
}
networkEventsFlow.value =
networkEventsFlow.value.copy(
vpnStatus = it.status,
config = it.tunnelConfig,
)
}
}
@ -286,6 +280,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
isEthernetConnected = true,
)
}
is NetworkStatus.CapabilitiesChanged -> {
Timber.i("Ethernet capabilities changed")
networkEventsFlow.value =
@ -293,6 +288,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
isEthernetConnected = true,
)
}
is NetworkStatus.Unavailable -> {
networkEventsFlow.value =
networkEventsFlow.value.copy(
@ -314,19 +310,24 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
isWifiConnected = true,
)
}
is NetworkStatus.CapabilitiesChanged -> {
Timber.i("Wifi capabilities changed")
networkEventsFlow.value =
networkEventsFlow.value.copy(
isWifiConnected = true,
)
val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: ""
Timber.i("Detected SSID: $ssid")
networkEventsFlow.value =
networkEventsFlow.value.copy(
currentNetworkSSID = ssid,
)
val ssid = wifiService.getNetworkName(it.networkCapabilities)
ssid?.let {
Timber.i("Detected SSID: $ssid")
appDataRepository.appState.setCurrentSsid(ssid)
networkEventsFlow.value =
networkEventsFlow.value.copy(
currentNetworkSSID = ssid,
)
} ?: Timber.w("Failed to read ssid")
}
is NetworkStatus.Unavailable -> {
networkEventsFlow.value =
networkEventsFlow.value.copy(
@ -338,64 +339,84 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
}
}
private suspend fun getMobileDataTunnel(): TunnelConfig? {
return appDataRepository.tunnels.findByMobileDataTunnel().firstOrNull()
}
private suspend fun getSsidTunnel(ssid: String): TunnelConfig? {
return appDataRepository.tunnels.findByTunnelNetworksName(ssid).firstOrNull()
}
private suspend fun manageVpn() {
networkEventsFlow.collectLatest {
networkEventsFlow.collectLatest { watcherState ->
val autoTunnel = "Auto-tunnel watcher"
if (!it.settings.isAutoTunnelPaused && it.settings.defaultTunnel != null) {
delay(Constants.TOGGLE_TUNNEL_DELAY)
if (!watcherState.settings.isAutoTunnelPaused) {
//delay for rapid network state changes and then collect latest
delay(Constants.WATCHER_COLLECTION_DELAY)
when {
((it.isEthernetConnected &&
it.settings.isTunnelOnEthernetEnabled &&
!it.isVpnConnected)) -> {
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
Timber.i("$autoTunnel condition 1 met")
watcherState.isEthernetConditionMet() -> {
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
serviceManager.startVpnServiceForeground(this)
}
(!it.isEthernetConnected &&
it.settings.isTunnelOnMobileDataEnabled &&
!it.isWifiConnected &&
it.isMobileDataConnected &&
!it.isVpnConnected) -> {
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
Timber.i("$autoTunnel condition 2 met")
watcherState.isMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel on on mobile data condition met")
serviceManager.startVpnServiceForeground(this, getMobileDataTunnel()?.id)
}
(!it.isEthernetConnected &&
!it.settings.isTunnelOnMobileDataEnabled &&
!it.isWifiConnected &&
it.isVpnConnected) -> {
ServiceManager.stopVpnService(this)
Timber.i("$autoTunnel condition 3 met")
watcherState.isTunnelNotMobileDataPreferredConditionMet() -> {
getMobileDataTunnel()?.let {
Timber.i("$autoTunnel - tunnel connected on mobile data is not preferred condition met, switching to preferred")
serviceManager.startVpnServiceForeground(
this,
getMobileDataTunnel()?.id,
)
}
}
(!it.isEthernetConnected &&
it.isWifiConnected &&
!it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID) &&
it.settings.isTunnelOnWifiEnabled &&
(!it.isVpnConnected)) -> {
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
Timber.i("$autoTunnel condition 4 met")
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
serviceManager.stopVpnService(this)
}
(!it.isEthernetConnected &&
(it.isWifiConnected &&
it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) &&
(it.isVpnConnected)) -> {
ServiceManager.stopVpnService(this)
Timber.i("$autoTunnel condition 5 met")
watcherState.isTunnelNotWifiNamePreferredMet(watcherState.currentNetworkSSID) -> {
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")
serviceManager.startVpnServiceForeground(this, it.id)
} ?: suspend {
Timber.i("No tunnel associated with this SSID, using defaults")
if (appDataRepository.getPrimaryOrFirstTunnel()?.name != vpnService.name) {
serviceManager.startVpnServiceForeground(this)
}
}.invoke()
}
(!it.isEthernetConnected &&
(it.isWifiConnected &&
!it.settings.isTunnelOnWifiEnabled &&
(it.isVpnConnected))) -> {
ServiceManager.stopVpnService(this)
Timber.i("$autoTunnel condition 6 met")
watcherState.isUntrustedWifiConditionMet() -> {
Timber.i("$autoTunnel - tunnel on untrusted wifi condition met")
serviceManager.startVpnServiceForeground(
this,
getSsidTunnel(watcherState.currentNetworkSSID)?.id,
)
}
(!it.isEthernetConnected &&
!it.isWifiConnected &&
!it.isMobileDataConnected &&
(it.isVpnConnected)) -> {
ServiceManager.stopVpnService(this)
Timber.i("$autoTunnel condition 7 met")
watcherState.isTrustedWifiConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off")
serviceManager.stopVpnService(this)
}
watcherState.isTunnelOffOnWifiConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off")
serviceManager.stopVpnService(this)
}
watcherState.isTunnelOffOnNoConnectivityMet() -> {
Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off")
serviceManager.stopVpnService(this)
}
else -> {
Timber.i("$autoTunnel no condition met")
Timber.i("$autoTunnel - no condition met")
}
}
}

View File

@ -5,10 +5,9 @@ import android.content.Intent
import android.os.Bundle
import androidx.core.app.ServiceCompat
import androidx.lifecycle.lifecycleScope
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
@ -21,30 +20,30 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class WireGuardTunnelService : ForegroundService() {
private val foregroundId = 123
@Inject lateinit var vpnService: VpnService
@Inject
lateinit var vpnService: VpnService
@Inject lateinit var settingsRepository: SettingsRepository
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
@Inject lateinit var notificationService: NotificationService
@Inject
lateinit var notificationService: NotificationService
private lateinit var job: Job
private var tunnelName: String = ""
private var didShowConnected = false
override fun onCreate() {
super.onCreate()
lifecycleScope.launch(Dispatchers.Main) {
if (tunnelConfigRepository.getAll().isNotEmpty()) {
//TODO fix this to not launch if AOVPN
if (appDataRepository.tunnels.count() != 0) {
launchVpnNotification()
}
}
@ -53,74 +52,69 @@ class WireGuardTunnelService : ForegroundService() {
override fun startService(extras: Bundle?) {
super.startService(extras)
cancelJob()
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
val tunnelConfig = tunnelConfigString?.let { TunnelConfig.from(it) }
tunnelName = tunnelConfig?.name ?: ""
job =
lifecycleScope.launch(Dispatchers.IO) {
launch {
if (tunnelConfig != null) {
try {
tunnelName = tunnelConfig.name
vpnService.startTunnel(tunnelConfig)
} catch (e: Exception) {
Timber.e("Problem starting tunnel: ${e.message}")
stopService(extras)
}
} else {
Timber.i("Tunnel config null, starting default tunnel or first")
val settings = settingsRepository.getSettings()
val tunnels = tunnelConfigRepository.getAll()
if (settings.isAlwaysOnVpnEnabled) {
val tunnel =
if (settings.defaultTunnel != null) {
TunnelConfig.from(settings.defaultTunnel!!)
} else if (tunnels.isNotEmpty()) {
tunnels.first()
} else {
null
}
if (tunnel != null) {
tunnelName = tunnel.name
vpnService.startTunnel(tunnel)
}
} else {
launchAlwaysOnDisabledNotification()
}
val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY)
if (vpnService.getState() == Tunnel.State.UP) {
vpnService.stopTunnel()
}
vpnService.startTunnel(
tunnelId?.let {
appDataRepository.tunnels.getById(it)
},
)
}
// TODO add failed to connect notification
launch {
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)
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 -> {}
else -> {}
}
}
}
handshakeNotifications()
}
}
}
//TODO improve tunnel notifications
private suspend fun handshakeNotifications() {
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
}
}
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
true -> {
}
else -> {}
}
}
if (state.status == Tunnel.State.UP && state.tunnelConfig?.name != tunnelName) {
tunnelName = state.tunnelConfig?.name
launchVpnNotification(
getString(R.string.tunnel_start_title),
"${getString(R.string.tunnel_start_text)} - $tunnelName",
)
}
}
}
private fun launchAlwaysOnDisabledNotification() {
launchVpnNotification(title = this.getString(R.string.vpn_connection_failed),
description = this.getString(R.string.always_on_disabled))
launchVpnNotification(
title = this.getString(R.string.vpn_connection_failed),
description = this.getString(R.string.always_on_disabled),
)
}
override fun stopService(extras: Bundle?) {
@ -161,12 +155,12 @@ class WireGuardTunnelService : ForegroundService() {
channelId = getString(R.string.vpn_channel_id),
channelName = getString(R.string.vpn_channel_name),
action =
PendingIntent.getBroadcast(
this,
0,
Intent(this, NotificationActionReceiver::class.java),
PendingIntent.FLAG_IMMUTABLE,
),
PendingIntent.getBroadcast(
this,
0,
Intent(this, NotificationActionReceiver::class.java),
PendingIntent.FLAG_IMMUTABLE,
),
actionText = getString(R.string.restart),
title = getString(R.string.vpn_connection_failed),
onGoing = false,

View File

@ -53,6 +53,7 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
}
}
}
else -> {
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
@ -117,7 +118,7 @@ inline fun <Result> Flow<NetworkStatus>.map(
crossinline onUnavailable: suspend (network: Network) -> Result,
crossinline onAvailable: suspend (network: Network) -> Result,
crossinline onCapabilitiesChanged:
suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result
suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result
): Flow<Result> = map { status ->
when (status) {
is NetworkStatus.Unavailable -> onUnavailable(status.network)

View File

@ -45,10 +45,10 @@ class WireGuardNotification @Inject constructor(@ApplicationContext private val
): Notification {
val channel =
NotificationChannel(
channelId,
channelName,
importance,
)
channelId,
channelName,
importance,
)
.let {
it.description = title
it.enableLights(lights)

View File

@ -1,80 +1,66 @@
package com.zaneschepke.wireguardautotunnel.service.shortcut
import android.os.Bundle
import android.view.View
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
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.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class ShortcutsActivity : ComponentActivity() {
@Inject lateinit var settingsRepository: SettingsRepository
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
@Inject
lateinit var appDataRepository: AppDataRepository
private suspend fun toggleWatcherServicePause() {
val settings = settingsRepository.getSettings()
if (settings.isAutoTunnelEnabled) {
val pauseAutoTunnel = !settings.isAutoTunnelPaused
settingsRepository.save(
settings.copy(
isAutoTunnelPaused = pauseAutoTunnel,
),
)
}
}
@Inject
lateinit var serviceManager: ServiceManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(View(this))
if (
intent
.getStringExtra(CLASS_NAME_EXTRA_KEY)
.equals(WireGuardTunnelService::class.java.simpleName)
) {
lifecycleScope.launch(Dispatchers.Main) {
val settings = settingsRepository.getSettings()
if (settings.isShortcutsEnabled) {
try {
lifecycleScope.launch(Dispatchers.Main) {
val settings = appDataRepository.settings.getSettings()
if (settings.isShortcutsEnabled) {
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
WireGuardTunnelService::class.java.simpleName -> {
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
val tunnelConfig =
if (tunnelName != null) {
tunnelConfigRepository.getAll().firstOrNull {
it.name == tunnelName
}
} else {
if (settings.defaultTunnel == null) {
tunnelConfigRepository.getAll().first()
} else {
TunnelConfig.from(settings.defaultTunnel!!)
}
val tunnelConfig = tunnelName?.let {
appDataRepository.tunnels.getAll().firstOrNull {
it.name == tunnelName
}
tunnelConfig ?: return@launch
toggleWatcherServicePause()
when (intent.action) {
Action.STOP.name ->
ServiceManager.stopVpnService(
this@ShortcutsActivity,
)
Action.START.name ->
ServiceManager.startVpnServiceForeground(
this@ShortcutsActivity,
tunnelConfig.toString(),
)
}
} catch (e: Exception) {
Timber.e(e.message)
finish()
when (intent.action) {
Action.START.name -> serviceManager.startVpnServiceForeground(
this@ShortcutsActivity, tunnelConfig?.id, isManualStart = true,
)
Action.STOP.name -> serviceManager.stopVpnService(
this@ShortcutsActivity,
isManualStop = true,
)
}
}
WireGuardConnectivityWatcherService::class.java.simpleName -> {
when (intent.action) {
Action.START.name -> appDataRepository.settings.save(
settings.copy(
isAutoTunnelPaused = false,
),
)
Action.STOP.name -> appDataRepository.settings.save(
settings.copy(
isAutoTunnelPaused = true,
),
)
}
}
}
}

View File

@ -0,0 +1,106 @@
package com.zaneschepke.wireguardautotunnel.service.tile
import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
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
@AndroidEntryPoint
class AutoTunnelControlTile : TileService() {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var serviceManager: ServiceManager
private val scope = CoroutineScope(Dispatchers.IO)
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))
}
}
false -> {
setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled))
setUnavailable()
}
}
}
}
}
override fun onDestroy() {
super.onDestroy()
scope.cancel()
}
override fun onTileRemoved() {
super.onTileRemoved()
scope.cancel()
}
override fun onClick() {
super.onClick()
unlockAndRun {
scope.launch {
try {
appDataRepository.toggleWatcherServicePause()
} catch (e: Exception) {
Timber.e(e.message)
} finally {
cancel()
}
}
}
}
private fun setActive() {
qsTile.state = Tile.STATE_ACTIVE
qsTile.updateTile()
}
private fun setInactive() {
qsTile.state = Tile.STATE_INACTIVE
qsTile.updateTile()
}
private fun setUnavailable() {
manualStartConfig = null
qsTile.state = Tile.STATE_UNAVAILABLE
qsTile.updateTile()
}
private fun setTileDescription(description: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
qsTile.subtitle = description
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
qsTile.stateDescription = description
}
qsTile.updateTile()
}
}

View File

@ -5,8 +5,7 @@ import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import dagger.hilt.android.AndroidEntryPoint
@ -18,40 +17,44 @@ import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class TunnelControlTile() : TileService() {
class TunnelControlTile : TileService() {
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject lateinit var settingsRepository: SettingsRepository
@Inject
lateinit var vpnService: VpnService
@Inject lateinit var vpnService: VpnService
@Inject
lateinit var serviceManager: ServiceManager
private val scope = CoroutineScope(Dispatchers.IO)
private var tunnelName: String? = null
private var manualStartConfig: TunnelConfig? = null
override fun onStartListening() {
super.onStartListening()
Timber.d("On start listening called")
scope.launch {
vpnService.vpnState.collect {
vpnService.vpnState.collect { it ->
when (it.status) {
Tunnel.State.UP -> setActive()
Tunnel.State.DOWN -> setInactive()
Tunnel.State.UP -> {
setActive()
it.tunnelConfig?.name?.let { name -> setTileDescription(name) }
}
Tunnel.State.DOWN -> {
setInactive()
val config = appDataRepository.getStartTunnelConfig()?.also { config ->
manualStartConfig = config
} ?: appDataRepository.getPrimaryOrFirstTunnel()
config?.let {
setTileDescription(it.name)
} ?: setUnavailable()
}
else -> setInactive()
}
val tunnels = tunnelConfigRepository.getAll()
if (tunnels.isEmpty()) {
setUnavailable()
return@collect
}
tunnelName = it.name.run {
val settings = settingsRepository.getSettings()
if (settings.defaultTunnel != null) {
TunnelConfig.from(settings.defaultTunnel!!).name
} else tunnels.firstOrNull()?.name
}
setTileDescription(tunnelName ?: "")
}
}
}
@ -71,18 +74,11 @@ class TunnelControlTile() : TileService() {
unlockAndRun {
scope.launch {
try {
val defaultTunnel = settingsRepository.getSettings().defaultTunnel
val config = defaultTunnel ?: run {
val tunnelConfigs = tunnelConfigRepository.getAll()
return@run tunnelConfigs.find { it.name == tunnelName }
}
toggleWatcherServicePause()
if (vpnService.getState() == Tunnel.State.UP) {
ServiceManager.stopVpnService(this@TunnelControlTile)
serviceManager.stopVpnService(this@TunnelControlTile, isManualStop = true)
} else {
ServiceManager.startVpnServiceForeground(
this@TunnelControlTile,
config.toString(),
serviceManager.startVpnServiceForeground(
this@TunnelControlTile, manualStartConfig?.id, isManualStart = true,
)
}
} catch (e: Exception) {
@ -94,20 +90,6 @@ class TunnelControlTile() : TileService() {
}
}
private fun toggleWatcherServicePause() {
scope.launch {
val settings = settingsRepository.getSettings()
if (settings.isAutoTunnelEnabled) {
val pauseAutoTunnel = !settings.isAutoTunnelPaused
settingsRepository.save(
settings.copy(
isAutoTunnelPaused = pauseAutoTunnel,
),
)
}
}
}
private fun setActive() {
qsTile.state = Tile.STATE_ACTIVE
qsTile.updateTile()
@ -119,6 +101,7 @@ class TunnelControlTile() : TileService() {
}
private fun setUnavailable() {
manualStartConfig = null
qsTile.state = Tile.STATE_UNAVAILABLE
qsTile.updateTile()
}

View File

@ -5,7 +5,7 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import kotlinx.coroutines.flow.StateFlow
interface VpnService : Tunnel {
suspend fun startTunnel(tunnelConfig: TunnelConfig): Tunnel.State
suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): Tunnel.State
suspend fun stopTunnel()

View File

@ -2,12 +2,10 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Statistics
import com.wireguard.android.backend.Tunnel
import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
data class VpnState(
val status: Tunnel.State = Tunnel.State.DOWN,
val name: String = "",
val config: Config? = null,
val tunnelConfig: TunnelConfig? = null,
val statistics: Statistics? = null
)

View File

@ -4,10 +4,9 @@ import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Statistics
import com.wireguard.android.backend.Tunnel.State
import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.Kernel
import com.zaneschepke.wireguardautotunnel.module.Userspace
import com.zaneschepke.wireguardautotunnel.util.Constants
@ -25,9 +24,9 @@ import javax.inject.Inject
class WireGuardTunnel
@Inject
constructor(
@Userspace private val userspaceBackend : Backend,
@Userspace private val userspaceBackend: Backend,
@Kernel private val kernelBackend: Backend,
private val settingsRepository: SettingsRepository
private val appDataRepository: AppDataRepository,
) : VpnService {
private val _vpnState = MutableStateFlow(VpnState())
override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow()
@ -36,15 +35,13 @@ constructor(
private lateinit var statsJob: Job
private var config: Config? = null
private var backend: Backend = userspaceBackend
private var backendIsUserspace = true
init {
scope.launch {
settingsRepository.getSettingsFlow().collect {
appDataRepository.settings.getSettingsFlow().collect {
if (it.isKernelEnabled && backendIsUserspace) {
Timber.d("Setting kernel backend")
backend = kernelBackend
@ -58,20 +55,21 @@ constructor(
}
}
override suspend fun startTunnel(tunnelConfig: TunnelConfig): State {
override suspend fun startTunnel(tunnelConfig: TunnelConfig?): State {
return try {
stopTunnelOnConfigChange(tunnelConfig)
emitTunnelName(tunnelConfig.name)
config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
emitTunnelConfig(config)
val state =
backend.setState(
this,
State.UP,
config,
)
emitTunnelState(state)
state
//TODO we need better error handling here
val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel()
if (config != null) {
emitTunnelConfig(config)
val wgConfig = TunnelConfig.configFromQuick(config.wgQuick)
val state =
backend.setState(
this,
State.UP,
wgConfig,
)
state
} else throw Exception("No tunnels")
} catch (e: BackendException) {
Timber.e("Failed to start tunnel with error: ${e.message}")
State.DOWN
@ -94,32 +92,14 @@ constructor(
)
}
private suspend fun emitTunnelName(name: String) {
private suspend fun emitTunnelConfig(tunnelConfig: TunnelConfig?) {
_vpnState.emit(
_vpnState.value.copy(
name = name,
tunnelConfig = tunnelConfig,
),
)
}
private suspend fun emitTunnelConfig(config : Config?) {
_vpnState.emit(
_vpnState.value.copy(
config = config,
),
)
}
private suspend fun stopTunnelOnConfigChange(tunnelConfig: TunnelConfig) {
if (getState() == State.UP && _vpnState.value.name != tunnelConfig.name) {
stopTunnel()
}
}
override fun getName(): String {
return _vpnState.value.name
}
override suspend fun stopTunnel() {
try {
if (getState() == State.UP) {
@ -135,10 +115,14 @@ constructor(
return backend.getState(this)
}
override fun getName(): String {
return _vpnState.value.tunnelConfig?.name ?: ""
}
override fun onStateChange(state: State) {
val tunnel = this
emitTunnelState(state)
WireGuardAutoTunnel.requestTileServiceStateUpdate(WireGuardAutoTunnel.instance)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance)
if (state == State.UP) {
statsJob =
scope.launch {

View File

@ -13,7 +13,6 @@ 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.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import dagger.hilt.android.lifecycle.HiltViewModel
@ -21,7 +20,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.time.Instant
import javax.inject.Inject
@ -32,30 +30,33 @@ class AppViewModel
constructor(
private val application: Application,
) : ViewModel() {
val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance)
private val _appUiState = MutableStateFlow(AppUiState(
vpnPermissionAccepted = vpnIntent == null
))
private val _appUiState = MutableStateFlow(
AppUiState(
vpnPermissionAccepted = vpnIntent == null,
),
)
val appUiState = _appUiState.asStateFlow()
fun isRequiredPermissionGranted() : Boolean {
val allAccepted = (_appUiState.value.vpnPermissionAccepted && _appUiState.value.vpnPermissionAccepted)
if(!allAccepted) requestPermissions()
fun isRequiredPermissionGranted(): Boolean {
val allAccepted =
(_appUiState.value.vpnPermissionAccepted && _appUiState.value.vpnPermissionAccepted)
if (!allAccepted) requestPermissions()
return allAccepted
}
private fun requestPermissions() {
_appUiState.value = _appUiState.value.copy(
requestPermissions = true
requestPermissions = true,
)
}
fun permissionsRequested() {
_appUiState.value = _appUiState.value.copy(
requestPermissions = false
requestPermissions = false,
)
}
@ -71,10 +72,10 @@ constructor(
showSnackbarMessage(application.getString(R.string.no_browser_detected))
}
}
fun onVpnPermissionAccepted() {
_appUiState.value = _appUiState.value.copy(
vpnPermissionAccepted = true
vpnPermissionAccepted = true,
)
}
@ -90,37 +91,41 @@ constructor(
application.startActivity(
Intent.createChooser(intent, application.getString(R.string.email_chooser)).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
})
},
)
} catch (e: ActivityNotFoundException) {
Timber.e(e)
showSnackbarMessage(application.getString(R.string.no_email_detected))
}
}
fun showSnackbarMessage(message : String) {
fun showSnackbarMessage(message: String) {
_appUiState.value = _appUiState.value.copy(
snackbarMessage = message,
snackbarMessageConsumed = false
snackbarMessageConsumed = false,
)
}
fun snackbarMessageConsumed() {
_appUiState.value = _appUiState.value.copy(
snackbarMessage = "",
snackbarMessageConsumed = true
snackbarMessageConsumed = true,
)
}
val logs = mutableStateListOf<LogMessage>()
fun readLogCatOutput() = viewModelScope.launch(viewModelScope.coroutineContext + Dispatchers.IO) {
launch {
Logcatter.logs {
logs.add(it)
if (logs.size > Constants.LOG_BUFFER_SIZE) {
logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt())
fun readLogCatOutput() =
viewModelScope.launch(viewModelScope.coroutineContext + Dispatchers.IO) {
launch {
Logcatter.logs {
logs.add(it)
if (logs.size > Constants.LOG_BUFFER_SIZE) {
logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt())
}
}
}
}
}
fun clearLogs() {
logs.clear()
@ -131,12 +136,13 @@ constructor(
val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt"
val content = logs.joinToString(separator = "\n")
FileUtils.saveFileToDownloads(application.applicationContext, content, fileName)
Toast.makeText(application, application.getString(R.string.logs_saved), Toast.LENGTH_SHORT).show()
Toast.makeText(application, application.getString(R.string.logs_saved), Toast.LENGTH_SHORT)
.show()
}
fun setNotificationPermissionAccepted(accepted: Boolean) {
_appUiState.value = _appUiState.value.copy(
notificationPermissionAccepted = accepted
notificationPermissionAccepted = accepted,
)
}
}

View File

@ -19,14 +19,10 @@ import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
@ -52,6 +48,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.options.OptionsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
@ -61,7 +58,6 @@ import com.zaneschepke.wireguardautotunnel.util.StringValue
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import xyz.teamgravity.pin_lock_compose.PinLock
import xyz.teamgravity.pin_lock_compose.PinManager
import javax.inject.Inject
@ -74,6 +70,9 @@ class MainActivity : AppCompatActivity() {
@Inject
lateinit var settingsRepository: SettingsRepository
@Inject
lateinit var serviceManager: ServiceManager
@OptIn(
ExperimentalPermissionsApi::class,
)
@ -85,10 +84,10 @@ class MainActivity : AppCompatActivity() {
// load preferences into memory and init data
lifecycleScope.launch {
dataStoreManager.init()
WireGuardAutoTunnel.requestTileServiceStateUpdate(this@MainActivity)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(this@MainActivity)
val settings = settingsRepository.getSettings()
if (settings.isAutoTunnelEnabled) {
ServiceManager.startWatcherService(application.applicationContext)
serviceManager.startWatcherService(application.applicationContext)
}
}
setContent {
@ -101,7 +100,6 @@ class MainActivity : AppCompatActivity() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) else null
val focusRequester = remember { FocusRequester() }
val snackbarHostState = remember { SnackbarHostState() }
val vpnActivityResultState =
@ -150,7 +148,7 @@ class MainActivity : AppCompatActivity() {
appViewModel.setNotificationPermissionAccepted(
notificationPermissionState?.status?.isGranted ?: true,
)
if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) appViewModel.readLogCatOutput()
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) appViewModel.readLogCatOutput()
}
LaunchedEffect(appUiState.snackbarMessageConsumed) {
@ -160,6 +158,8 @@ class MainActivity : AppCompatActivity() {
}
}
val focusRequester = remember { FocusRequester() }
Scaffold(
snackbarHost = {
SnackbarHost(snackbarHostState) { snackbarData: SnackbarData ->
@ -176,19 +176,21 @@ class MainActivity : AppCompatActivity() {
//TODO refactor
modifier = Modifier
.focusable()
.focusProperties { when(navBackStackEntry?.destination?.route) {
Screen.Lock.route -> Unit
else -> up = focusRequester }
},
.focusProperties {
when (navBackStackEntry?.destination?.route) {
Screen.Lock.route -> Unit
else -> up = focusRequester
}
},
bottomBar = {
BottomNavBar(
navController,
listOf(
Screen.Main.navItem,
Screen.Settings.navItem,
Screen.Support.navItem,
),
)
BottomNavBar(
navController,
listOf(
Screen.Main.navItem,
Screen.Settings.navItem,
Screen.Support.navItem,
),
)
},
) { padding ->
NavHost(
@ -215,7 +217,7 @@ class MainActivity : AppCompatActivity() {
SettingsScreen(
appViewModel = appViewModel,
navController = navController,
focusRequester = focusRequester
focusRequester = focusRequester,
)
}
composable(
@ -235,14 +237,28 @@ class MainActivity : AppCompatActivity() {
if (!id.isNullOrBlank()) {
ConfigScreen(
navController = navController,
id = id,
tunnelId = id,
appViewModel = appViewModel,
focusRequester = focusRequester,
)
}
}
composable("${Screen.Option.route}/{id}") {
val id = it.arguments?.getString("id")
if (!id.isNullOrBlank()) {
OptionsScreen(
navController = navController,
tunnelId = id,
appViewModel = appViewModel,
focusRequester = focusRequester,
)
}
}
composable(Screen.Lock.route) {
PinLockScreen(navController = navController, appViewModel = appViewModel)
PinLockScreen(
navController = navController,
appViewModel = appViewModel,
)
}
}
}

View File

@ -32,9 +32,12 @@ sealed class Screen(val route: String) {
route = route,
icon = Icons.Rounded.QuestionMark,
)
data object Logs : Screen("support/logs")
}
data object Config : Screen("config")
data object Lock : Screen("lock")
data object Option : Screen("option")
}

View File

@ -10,8 +10,6 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
@Composable
fun ClickableIconButton(
@ -31,7 +29,10 @@ fun ClickableIconButton(
imageVector = icon,
contentDescription = icon.name,
modifier =
Modifier.size(ButtonDefaults.IconSize).weight(1f, false).clickable {
Modifier
.size(ButtonDefaults.IconSize)
.weight(1f, false)
.clickable {
if (enabled) {
onIconClick()
}

View File

@ -34,16 +34,19 @@ fun RowListItem(
) {
Box(
modifier =
Modifier.animateContentSize()
.clip(RoundedCornerShape(30.dp))
.combinedClickable(
onClick = { onClick() },
onLongClick = { onHold() },
),
Modifier
.animateContentSize()
.clip(RoundedCornerShape(30.dp))
.combinedClickable(
onClick = { onClick() },
onLongClick = { onHold() },
),
) {
Column {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 15.dp, vertical = 5.dp),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 15.dp, vertical = 5.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
@ -59,8 +62,9 @@ fun RowListItem(
statistics?.peers()?.forEach {
Row(
modifier =
Modifier.fillMaxWidth()
.padding(end = 10.dp, bottom = 10.dp, start = 10.dp),
Modifier
.fillMaxWidth()
.padding(end = 10.dp, bottom = 10.dp, start = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly,
) {

View File

@ -63,17 +63,18 @@ fun SearchBar(onQuery: (queryString: String) -> Unit) {
},
maxLines = 1,
colors =
TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
),
TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
),
placeholder = { Text(text = stringResource(R.string.hint_search_packages)) },
textStyle = MaterialTheme.typography.bodySmall,
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
modifier =
Modifier.fillMaxWidth()
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape),
Modifier
.fillMaxWidth()
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape),
)
}

View File

@ -27,10 +27,10 @@ fun ConfigurationTextBox(
maxLines = 1,
placeholder = { Text(hint) },
keyboardOptions =
KeyboardOptions(
capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done,
),
KeyboardOptions(
capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done,
),
keyboardActions = keyboardActions,
)
}

View File

@ -9,6 +9,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
@Composable
@ -21,11 +22,13 @@ fun ConfigurationToggle(
modifier: Modifier = Modifier
) {
Row(
modifier = Modifier.fillMaxWidth().padding(padding),
modifier = Modifier
.fillMaxWidth()
.padding(padding),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(label)
Text(label, textAlign = TextAlign.Start)
Switch(
modifier = modifier,
enabled = enabled,

View File

@ -30,9 +30,9 @@ fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavIte
}
NavigationBar(
containerColor = if(!showBottomBar) Color.Transparent else MaterialTheme.colorScheme.background,
containerColor = if (!showBottomBar) Color.Transparent else MaterialTheme.colorScheme.background,
) {
if(showBottomBar) bottomNavItems.forEach { item ->
if (showBottomBar) bottomNavItems.forEach { item ->
val selected = item.route == backStackEntry.value?.destination?.route
NavigationBarItem(

View File

@ -21,26 +21,32 @@ fun AuthorizationPrompt(onSuccess: () -> Unit, onFailure: () -> Unit, onError: (
onError("Biometrics not available")
false
}
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
onError("Biometrics not created")
false
}
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
onError("Biometric hardware not found")
false
}
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
onError("Biometric security update required")
false
}
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
onError("Biometrics not supported")
false
}
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
onError("Biometrics status unknown")
false
}
BiometricManager.BIOMETRIC_SUCCESS -> true
else -> false
}

View File

@ -20,10 +20,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
@Composable
@ -35,17 +33,20 @@ fun CustomSnackBar(
Snackbar(
containerColor = containerColor,
modifier =
Modifier.fillMaxWidth(
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f,
)
.padding(bottom = 100.dp),
Modifier
.fillMaxWidth(
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f,
)
.padding(bottom = 100.dp),
shape = RoundedCornerShape(16.dp),
) {
CompositionLocalProvider(
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr,
) {
Row(
modifier = Modifier.width(IntrinsicSize.Max).height(IntrinsicSize.Min),
modifier = Modifier
.width(IntrinsicSize.Max)
.height(IntrinsicSize.Min),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
) {

View File

@ -16,7 +16,10 @@ fun LoadingScreen() {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier = Modifier.fillMaxSize().focusable().padding(),
modifier = Modifier
.fillMaxSize()
.focusable()
.padding(),
) {
Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() }
}

View File

@ -12,9 +12,14 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun LogTypeLabel(color : Color, content: @Composable () -> Unit,) {
fun LogTypeLabel(color: Color, content: @Composable () -> Unit) {
Box(
modifier = Modifier.size(20.dp).clip(RoundedCornerShape(2.dp)).background(color), contentAlignment = Alignment.Center) {
modifier = Modifier
.size(20.dp)
.clip(RoundedCornerShape(2.dp))
.background(color),
contentAlignment = Alignment.Center,
) {
content()
}
}

View File

@ -15,7 +15,7 @@ import androidx.compose.ui.unit.sp
fun SectionTitle(title: String, padding: Dp) {
Text(
title,
textAlign = TextAlign.Center,
textAlign = TextAlign.Start,
style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.ExtraBold),
modifier = Modifier.padding(padding, bottom = 5.dp, top = 5.dp),
)

View File

@ -18,11 +18,11 @@ data class InterfaceProxy(
addresses = i.addresses.joinToString(", ").trim(),
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
listenPort =
if (i.listenPort.isPresent) {
i.listenPort.get().toString().trim()
} else {
""
},
if (i.listenPort.isPresent) {
i.listenPort.get().toString().trim()
} else {
""
},
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "",
)
}

View File

@ -14,23 +14,23 @@ data class PeerProxy(
return PeerProxy(
publicKey = peer.publicKey.toBase64(),
preSharedKey =
if (peer.preSharedKey.isPresent) {
peer.preSharedKey.get().toBase64().trim()
} else {
""
},
if (peer.preSharedKey.isPresent) {
peer.preSharedKey.get().toBase64().trim()
} else {
""
},
persistentKeepalive =
if (peer.persistentKeepalive.isPresent) {
peer.persistentKeepalive.get().toString().trim()
} else {
""
},
if (peer.persistentKeepalive.isPresent) {
peer.persistentKeepalive.get().toString().trim()
} else {
""
},
endpoint =
if (peer.endpoint.isPresent) {
peer.endpoint.get().toString().trim()
} else {
""
},
if (peer.endpoint.isPresent) {
peer.endpoint.get().toString().trim()
} else {
""
},
allowedIps = peer.allowedIps.joinToString(", ").trim(),
)
}

View File

@ -94,7 +94,7 @@ fun ConfigScreen(
focusRequester: FocusRequester,
navController: NavController,
appViewModel: AppViewModel,
id: String
tunnelId: String
) {
val context = LocalContext.current
val clipboardManager: ClipboardManager = LocalClipboardManager.current
@ -105,7 +105,7 @@ fun ConfigScreen(
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
LaunchedEffect(Unit) { viewModel.init(id) }
LaunchedEffect(Unit) { viewModel.init(tunnelId) }
LaunchedEffect(uiState.loading) {
if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
@ -130,14 +130,14 @@ fun ConfigScreen(
val applicationButtonText = buildAnnotatedString {
append(stringResource(id = R.string.tunneling_apps))
append(": ")
if (uiState.isAllApplicationsEnabled) {
append(stringResource(id = R.string.all))
} else {
append("${uiState.checkedPackageNames.size} ")
(if (uiState.include) append(stringResource(id = R.string.included)) else append(
stringResource(id = R.string.excluded),
))
}
if (uiState.isAllApplicationsEnabled) {
append(stringResource(id = R.string.all))
} else {
append("${uiState.checkedPackageNames.size} ")
(if (uiState.include) append(stringResource(id = R.string.included)) else append(
stringResource(id = R.string.excluded),
))
}
}
if (showAuthPrompt) {
@ -173,8 +173,10 @@ fun ConfigScreen(
.fillMaxWidth()
.fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f),
) {
Column(modifier = Modifier
.fillMaxWidth()) {
Column(
modifier = Modifier
.fillMaxWidth(),
) {
Row(
modifier =
Modifier
@ -273,7 +275,7 @@ fun ConfigScreen(
modifier = Modifier.fillMaxSize(),
checked =
(uiState.checkedPackageNames.contains(
pack.packageName
pack.packageName,
)),
onCheckedChange = {
if (it) {
@ -311,11 +313,11 @@ fun ConfigScreen(
var fobColor by remember { mutableStateOf(secondaryColor) }
FloatingActionButton(
modifier =
Modifier.onFocusChanged {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
fobColor = if (it.isFocused) hoverColor else secondaryColor
}
},
Modifier.onFocusChanged {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
fobColor = if (it.isFocused) hoverColor else secondaryColor
}
},
onClick = {
viewModel.onSaveAllChanges().let {
when (it) {
@ -323,6 +325,7 @@ fun ConfigScreen(
appViewModel.showSnackbarMessage(it.data.message)
navController.navigate(Screen.Main.route)
}
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
}
}
@ -354,14 +357,14 @@ fun ConfigScreen(
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier =
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier
.fillMaxHeight(fillMaxHeight)
.fillMaxWidth(fillMaxWidth)
} else {
Modifier.fillMaxWidth(fillMaxWidth)
})
.padding(bottom = 10.dp),
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier
.fillMaxHeight(fillMaxHeight)
.fillMaxWidth(fillMaxWidth)
} else {
Modifier.fillMaxWidth(fillMaxWidth)
})
.padding(bottom = 10.dp),
) {
Column(
horizontalAlignment = Alignment.Start,
@ -390,10 +393,10 @@ fun ConfigScreen(
.clickable { showAuthPrompt = true },
value = uiState.interfaceProxy.privateKey,
visualTransformation =
if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated)
VisualTransformation.None
else PasswordVisualTransformation(),
enabled = (id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
if ((tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated)
VisualTransformation.None
else PasswordVisualTransformation(),
enabled = (tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
onValueChange = { value -> viewModel.onPrivateKeyChange(value) },
trailingIcon = {
IconButton(
@ -503,14 +506,14 @@ fun ConfigScreen(
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier =
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier
.fillMaxHeight(fillMaxHeight)
.fillMaxWidth(fillMaxWidth)
} else {
Modifier.fillMaxWidth(fillMaxWidth)
})
.padding(top = 10.dp, bottom = 10.dp),
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier
.fillMaxHeight(fillMaxHeight)
.fillMaxWidth(fillMaxWidth)
} else {
Modifier.fillMaxWidth(fillMaxWidth)
})
.padding(top = 10.dp, bottom = 10.dp),
) {
Column(
horizontalAlignment = Alignment.Start,

View File

@ -7,17 +7,14 @@ import android.content.pm.PackageManager
import android.os.Build
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wireguard.config.BadConfigException
import com.wireguard.config.Config
import com.wireguard.config.Interface
import com.wireguard.config.ParseException
import com.wireguard.config.Peer
import com.wireguard.crypto.Key
import com.wireguard.crypto.KeyPair
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
import com.zaneschepke.wireguardautotunnel.util.Constants
@ -30,7 +27,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ -40,8 +36,7 @@ class ConfigViewModel
@Inject
constructor(
private val application: Application,
private val tunnelConfigRepository: TunnelConfigRepository,
private val settingsRepository: SettingsRepository,
private val appDataRepository: AppDataRepository
) : ViewModel() {
private val packageManager = application.packageManager
@ -55,7 +50,8 @@ constructor(
val state =
if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
val tunnelConfig =
tunnelConfigRepository.getAll().firstOrNull { it.id.toString() == tunnelId }
appDataRepository.tunnels.getAll()
.firstOrNull { it.id.toString() == tunnelId }
if (tunnelConfig != null) {
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
val proxyPeers = config.peers.map { PeerProxy.from(it) }
@ -103,7 +99,7 @@ constructor(
fun onAddCheckedPackage(packageName: String) {
_uiState.value =
_uiState.value.copy(
checkedPackageNames = _uiState.value.checkedPackageNames + packageName
checkedPackageNames = _uiState.value.checkedPackageNames + packageName,
)
}
@ -114,7 +110,7 @@ constructor(
fun onRemoveCheckedPackage(packageName: String) {
_uiState.value =
_uiState.value.copy(
checkedPackageNames = _uiState.value.checkedPackageNames - packageName
checkedPackageNames = _uiState.value.checkedPackageNames - packageName,
)
}
@ -148,26 +144,16 @@ constructor(
}
private fun saveConfig(tunnelConfig: TunnelConfig) =
viewModelScope.launch { tunnelConfigRepository.save(tunnelConfig) }
viewModelScope.launch { appDataRepository.tunnels.save(tunnelConfig) }
private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) =
viewModelScope.launch {
if (tunnelConfig != null) {
saveConfig(tunnelConfig).join()
WireGuardAutoTunnel.requestTileServiceStateUpdate(application)
updateSettingsDefaultTunnel(tunnelConfig)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
}
}
private suspend fun updateSettingsDefaultTunnel(tunnelConfig: TunnelConfig) {
val settings = settingsRepository.getSettingsFlow().first()
if (settings.defaultTunnel != null) {
if (tunnelConfig.id == TunnelConfig.from(settings.defaultTunnel!!).id) {
settingsRepository.save(settings.copy(defaultTunnel = tunnelConfig.toString()))
}
}
}
private fun buildPeerListFromProxyPeers(): List<Peer> {
return _uiState.value.proxyPeers.map {
val builder = Peer.Builder()
@ -209,8 +195,12 @@ constructor(
val peerList = buildPeerListFromProxyPeers()
val wgInterface = buildInterfaceListFromProxyInterface()
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
val tunnelConfig = when(uiState.value.tunnel) {
null -> TunnelConfig(name = _uiState.value.tunnelName, wgQuick = config.toWgQuickString())
val tunnelConfig = when (uiState.value.tunnel) {
null -> TunnelConfig(
name = _uiState.value.tunnelName,
wgQuick = config.toWgQuickString(),
)
else -> uiState.value.tunnel!!.copy(
name = _uiState.value.tunnelName,
wgQuick = config.toWgQuickString(),
@ -229,10 +219,10 @@ constructor(
_uiState.value =
_uiState.value.copy(
proxyPeers =
_uiState.value.proxyPeers.update(
index,
_uiState.value.proxyPeers[index].copy(publicKey = value),
),
_uiState.value.proxyPeers.update(
index,
_uiState.value.proxyPeers[index].copy(publicKey = value),
),
)
}
@ -240,10 +230,10 @@ constructor(
_uiState.value =
_uiState.value.copy(
proxyPeers =
_uiState.value.proxyPeers.update(
index,
_uiState.value.proxyPeers[index].copy(preSharedKey = value),
),
_uiState.value.proxyPeers.update(
index,
_uiState.value.proxyPeers[index].copy(preSharedKey = value),
),
)
}
@ -251,10 +241,10 @@ constructor(
_uiState.value =
_uiState.value.copy(
proxyPeers =
_uiState.value.proxyPeers.update(
index,
_uiState.value.proxyPeers[index].copy(endpoint = value),
),
_uiState.value.proxyPeers.update(
index,
_uiState.value.proxyPeers[index].copy(endpoint = value),
),
)
}
@ -262,10 +252,10 @@ constructor(
_uiState.value =
_uiState.value.copy(
proxyPeers =
_uiState.value.proxyPeers.update(
index,
_uiState.value.proxyPeers[index].copy(allowedIps = value),
),
_uiState.value.proxyPeers.update(
index,
_uiState.value.proxyPeers[index].copy(allowedIps = value),
),
)
}
@ -273,10 +263,10 @@ constructor(
_uiState.value =
_uiState.value.copy(
proxyPeers =
_uiState.value.proxyPeers.update(
index,
_uiState.value.proxyPeers[index].copy(persistentKeepalive = value),
),
_uiState.value.proxyPeers.update(
index,
_uiState.value.proxyPeers[index].copy(persistentKeepalive = value),
),
)
}
@ -296,31 +286,31 @@ constructor(
_uiState.value =
_uiState.value.copy(
interfaceProxy =
_uiState.value.interfaceProxy.copy(
privateKey = keyPair.privateKey.toBase64(),
publicKey = keyPair.publicKey.toBase64(),
),
_uiState.value.interfaceProxy.copy(
privateKey = keyPair.privateKey.toBase64(),
publicKey = keyPair.publicKey.toBase64(),
),
)
}
fun onAddressesChanged(value: String) {
_uiState.value =
_uiState.value.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value)
interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value),
)
}
fun onListenPortChanged(value: String) {
_uiState.value =
_uiState.value.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value)
interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value),
)
}
fun onDnsServersChanged(value: String) {
_uiState.value =
_uiState.value.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value)
interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value),
)
}
@ -332,14 +322,14 @@ constructor(
private fun onInterfacePublicKeyChange(value: String) {
_uiState.value =
_uiState.value.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value)
interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value),
)
}
fun onPrivateKeyChange(value: String) {
_uiState.value =
_uiState.value.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value)
interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value),
)
if (NumberUtils.isValidKey(value)) {
val pair = KeyPair(Key.fromBase64(value))

View File

@ -38,8 +38,9 @@ import androidx.compose.material.icons.rounded.Bolt
import androidx.compose.material.icons.rounded.Circle
import androidx.compose.material.icons.rounded.CopyAll
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.Info
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material.icons.rounded.Smartphone
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
@ -123,7 +124,6 @@ fun MainScreen(
val sheetState = rememberModalBottomSheetState()
var showBottomSheet by remember { mutableStateOf(false) }
var showPrimaryChangeAlertDialog by remember { mutableStateOf(false) }
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
@ -197,30 +197,6 @@ fun MainScreen(
},
)
AnimatedVisibility(showPrimaryChangeAlertDialog) {
AlertDialog(
onDismissRequest = { showPrimaryChangeAlertDialog = false },
confirmButton = {
TextButton(
onClick = {
viewModel.onDefaultTunnelChange(selectedTunnel)
showPrimaryChangeAlertDialog = false
selectedTunnel = null
},
) {
Text(text = stringResource(R.string.okay))
}
},
dismissButton = {
TextButton(onClick = { showPrimaryChangeAlertDialog = false }) {
Text(text = stringResource(R.string.cancel))
}
},
title = { Text(text = stringResource(R.string.primary_tunnel_change)) },
text = { Text(text = stringResource(R.string.primary_tunnel_change_question)) },
)
}
AnimatedVisibility(showDeleteTunnelAlertDialog) {
AlertDialog(
onDismissRequest = { showDeleteTunnelAlertDialog = false },
@ -246,12 +222,12 @@ fun MainScreen(
}
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
if(appViewModel.isRequiredPermissionGranted()) {
if (appViewModel.isRequiredPermissionGranted()) {
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
}
}
if(uiState.loading) {
if (uiState.loading) {
return LoadingScreen()
}
@ -426,7 +402,7 @@ fun MainScreen(
icon,
icon.name,
modifier = Modifier
.padding(end = 10.dp)
.padding(end = 8.5.dp)
.size(25.dp),
tint =
if (uiState.settings.isAutoTunnelPaused) Color.Gray
@ -462,7 +438,7 @@ fun MainScreen(
) { tunnel ->
val leadingIconColor =
(if (
uiState.vpnState.name == tunnel.name &&
uiState.vpnState.tunnelConfig?.name == tunnel.name &&
uiState.vpnState.status == Tunnel.State.UP
) {
uiState.vpnState.statistics
@ -486,31 +462,31 @@ fun MainScreen(
val expanded = remember { mutableStateOf(false) }
RowListItem(
icon = {
if (uiState.settings.isTunnelConfigDefault(tunnel)) {
Icon(
Icons.Rounded.Star,
stringResource(R.string.status),
tint = leadingIconColor,
modifier = Modifier
.padding(end = 10.dp)
.size(20.dp),
)
val circleIcon = Icons.Rounded.Circle
val icon = if (tunnel.isPrimaryTunnel) {
Icons.Rounded.Star
} else if (tunnel.isMobileDataTunnel) {
Icons.Rounded.Smartphone
} else {
Icon(
Icons.Rounded.Circle,
stringResource(R.string.status),
tint = leadingIconColor,
modifier = Modifier
.padding(end = 15.dp)
.size(15.dp),
)
circleIcon
}
Icon(
icon,
icon.name,
tint = leadingIconColor,
modifier = Modifier
.padding(
end = if (icon == circleIcon) 12.5.dp else 10.dp,
start = if (icon == circleIcon) 2.5.dp else 0.dp,
)
.size(if (icon == circleIcon) 15.dp else 20.dp),
)
},
text = tunnel.name.truncateWithEllipsis(15),
text = tunnel.name.truncateWithEllipsis(Constants.ALLOWED_DISPLAY_NAME_LENGTH),
onHold = {
if (
(uiState.vpnState.status == Tunnel.State.UP) &&
(tunnel.name == uiState.vpnState.name)
(tunnel.name == uiState.vpnState.tunnelConfig?.name)
) {
appViewModel.showSnackbarMessage(Event.Message.TunnelOffAction.message)
return@RowListItem
@ -522,7 +498,7 @@ fun MainScreen(
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
if (
uiState.vpnState.status == Tunnel.State.UP &&
(uiState.vpnState.name == tunnel.name)
(uiState.vpnState.tunnelConfig?.name == tunnel.name)
) {
expanded.value = !expanded.value
}
@ -539,48 +515,27 @@ fun MainScreen(
!WireGuardAutoTunnel.isRunningOnAndroidTv()
) {
Row {
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
IconButton(
onClick = {
if (
uiState.settings.isAutoTunnelEnabled &&
!uiState.settings.isAutoTunnelPaused
) {
appViewModel.showSnackbarMessage(
Event.Message.AutoTunnelOffAction.message,
)
} else {
showPrimaryChangeAlertDialog = true
}
},
) {
val icon = Icons.Rounded.Star
Icon(
icon,
icon.name,
)
}
}
IconButton(
onClick = {
if (
uiState.settings.isAutoTunnelEnabled &&
uiState.settings.isTunnelConfigDefault(
tunnel,
) &&
!uiState.settings.isAutoTunnelPaused
) {
appViewModel.showSnackbarMessage(
Event.Message.AutoTunnelOffAction.message,
)
} else
} else {
navController.navigate(
"${Screen.Config.route}/${selectedTunnel?.id}",
"${Screen.Option.route}/${selectedTunnel?.id}",
)
}
},
) {
val icon = Icons.Rounded.Edit
Icon(icon, icon.name)
val icon = Icons.Rounded.Settings
Icon(
icon,
icon.name,
)
}
IconButton(
modifier = Modifier.focusable(),
@ -601,7 +556,7 @@ fun MainScreen(
val checked by remember {
derivedStateOf {
(uiState.vpnState.status == Tunnel.State.UP &&
tunnel.name == uiState.vpnState.name)
tunnel.name == uiState.vpnState.tunnelConfig?.name)
}
}
if (!checked) expanded.value = false
@ -618,32 +573,32 @@ fun MainScreen(
)
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Row {
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
IconButton(
onClick = {
if (uiState.settings.isAutoTunnelEnabled) {
appViewModel.showSnackbarMessage(
Event.Message.AutoTunnelOffAction.message,
)
} else {
selectedTunnel = tunnel
showPrimaryChangeAlertDialog = true
}
},
) {
val icon = Icons.Rounded.Star
Icon(
icon,
icon.name,
)
}
IconButton(
onClick = {
if (uiState.settings.isAutoTunnelEnabled) {
appViewModel.showSnackbarMessage(
Event.Message.AutoTunnelOffAction.message,
)
} else {
selectedTunnel = tunnel
navController.navigate(
"${Screen.Option.route}/${selectedTunnel?.id}",
)
}
},
) {
val icon = Icons.Rounded.Settings
Icon(
icon,
icon.name,
)
}
IconButton(
modifier = Modifier.focusRequester(focusRequester),
onClick = {
if (
uiState.vpnState.status == Tunnel.State.UP &&
(uiState.vpnState.name == tunnel.name)
(uiState.vpnState.tunnelConfig?.name == tunnel.name)
) {
expanded.value = !expanded.value
} else {
@ -656,25 +611,6 @@ fun MainScreen(
val icon = Icons.Rounded.Info
Icon(icon, icon.name)
}
IconButton(
onClick = {
if (
uiState.vpnState.status == Tunnel.State.UP &&
tunnel.name == uiState.vpnState.name
) {
appViewModel.showSnackbarMessage(
Event.Message.TunnelOffAction.message,
)
} else {
navController.navigate(
"${Screen.Config.route}/${tunnel.id}",
)
}
},
) {
val icon = Icons.Rounded.Edit
Icon(icon, icon.name)
}
IconButton(
onClick = { viewModel.onCopyTunnel(tunnel) },
) {
@ -685,7 +621,7 @@ fun MainScreen(
onClick = {
if (
uiState.vpnState.status == Tunnel.State.UP &&
tunnel.name == uiState.vpnState.name
tunnel.name == uiState.vpnState.tunnelConfig?.name
) {
appViewModel.showSnackbarMessage(
Event.Message.TunnelOffAction.message,
@ -699,7 +635,7 @@ fun MainScreen(
val icon = Icons.Rounded.Delete
Icon(
icon,
icon.name
icon.name,
)
}
TunnelSwitch()

View File

@ -7,13 +7,11 @@ import android.net.Uri
import android.provider.OpenableColumns
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.zxing.common.StringUtils
import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.util.Constants
@ -22,8 +20,6 @@ import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.Result
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
@ -39,19 +35,19 @@ class MainViewModel
@Inject
constructor(
private val application: Application,
private val tunnelConfigRepository: TunnelConfigRepository,
private val settingsRepository: SettingsRepository,
private val vpnService: VpnService
private val appDataRepository: AppDataRepository,
private val serviceManager: ServiceManager,
val vpnService: VpnService
) : ViewModel() {
val uiState =
combine(
settingsRepository.getSettingsFlow(),
tunnelConfigRepository.getTunnelConfigsFlow(),
vpnService.vpnState,
) { settings, tunnels, vpnState ->
MainUiState(settings, tunnels, vpnState, false)
}
appDataRepository.settings.getSettingsFlow(),
appDataRepository.tunnels.getTunnelConfigsFlow(),
vpnService.vpnState,
) { settings, tunnels, vpnState ->
MainUiState(settings, tunnels, vpnState, false)
}
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
@ -60,48 +56,46 @@ constructor(
private fun stopWatcherService() =
viewModelScope.launch(Dispatchers.IO) {
ServiceManager.stopWatcherService(application.applicationContext)
serviceManager.stopWatcherService(application.applicationContext)
}
fun onDelete(tunnel: TunnelConfig) {
viewModelScope.launch(Dispatchers.IO) {
val settings = settingsRepository.getSettings()
val isDefault = settings.isTunnelConfigDefault(tunnel)
if (tunnelConfigRepository.count() == 1 || isDefault) {
val settings = appDataRepository.settings.getSettings()
val isPrimary = tunnel.isPrimaryTunnel
if (appDataRepository.tunnels.count() == 1 || isPrimary) {
stopWatcherService()
settings.defaultTunnel = null
settings.isAutoTunnelEnabled = false
settings.isAlwaysOnVpnEnabled = false
saveSettings(settings)
resetTunnelSetting(settings)
}
tunnelConfigRepository.delete(tunnel)
WireGuardAutoTunnel.requestTileServiceStateUpdate(application)
appDataRepository.tunnels.delete(tunnel)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
}
}
private fun resetTunnelSetting(settings: Settings) {
saveSettings(
settings.copy(
isAutoTunnelEnabled = false,
isAlwaysOnVpnEnabled = false,
),
)
}
fun onTunnelStart(tunnelConfig: TunnelConfig) =
viewModelScope.launch(Dispatchers.IO) {
Timber.d("On start called!")
stopActiveTunnel().await()
startTunnel(tunnelConfig)
serviceManager.startVpnService(
application.applicationContext,
tunnelConfig.id,
isManualStart = true,
)
}
private fun startTunnel(tunnelConfig: TunnelConfig) =
viewModelScope.launch(Dispatchers.IO) {
Timber.d("Start tunnel via manager")
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
}
private fun stopActiveTunnel() =
viewModelScope.async(Dispatchers.IO) {
onTunnelStop()
delay(Constants.TOGGLE_TUNNEL_DELAY)
}
fun onTunnelStop() =
viewModelScope.launch(Dispatchers.IO) {
Timber.i("Stopping active tunnel")
ServiceManager.stopVpnService(application.applicationContext)
serviceManager.stopVpnService(application.applicationContext, isManualStop = true)
}
private fun validateConfigString(config: String) {
@ -145,6 +139,7 @@ constructor(
is Result.Success -> return it
}
}
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
else -> return Result.Error(Event.Error.InvalidFileExtension)
}
@ -186,23 +181,25 @@ constructor(
}
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
val firstTunnel = tunnelConfigRepository.count() == 0
val firstTunnel = appDataRepository.tunnels.count() == 0
saveTunnel(tunnelConfig)
if(firstTunnel) WireGuardAutoTunnel.requestTileServiceStateUpdate(application)
if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
}
fun pauseAutoTunneling() =
viewModelScope.launch {
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
}
fun resumeAutoTunneling() =
viewModelScope.launch {
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
}
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
tunnelConfigRepository.save(tunnelConfig)
appDataRepository.tunnels.save(tunnelConfig)
}
private fun getFileNameByCursor(context: Context, uri: Uri): String? {
@ -252,20 +249,17 @@ constructor(
}
private fun saveSettings(settings: Settings) =
viewModelScope.launch(Dispatchers.IO) { settingsRepository.save(settings) }
viewModelScope.launch(Dispatchers.IO) { appDataRepository.settings.save(settings) }
fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) =
viewModelScope.launch {
if (selectedTunnel != null) {
saveSettings(uiState.value.settings.copy(defaultTunnel = selectedTunnel.toString()))
.join()
WireGuardAutoTunnel.requestTileServiceStateUpdate(application)
}
}
fun onCopyTunnel(tunnel: TunnelConfig?) = viewModelScope.launch {
tunnel?.let {
saveTunnel(TunnelConfig(name = it.name.plus(NumberUtils.randomThree()), wgQuick = it.wgQuick))
saveTunnel(
TunnelConfig(
name = it.name.plus(NumberUtils.randomThree()),
wgQuick = it.wgQuick,
),
)
}
}
}

View File

@ -0,0 +1,287 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.options
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
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.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
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.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
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.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Result
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun OptionsScreen(
optionsViewModel: OptionsViewModel = hiltViewModel(),
navController: NavController,
appViewModel: AppViewModel,
focusRequester: FocusRequester,
tunnelId: String
) {
val scrollState = rememberScrollState()
val uiState by optionsViewModel.uiState.collectAsStateWithLifecycle()
val interactionSource = remember { MutableInteractionSource() }
val scope = rememberCoroutineScope()
val focusManager = LocalFocusManager.current
val screenPadding = 5.dp
val fillMaxWidth = .85f
var currentText by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
optionsViewModel.init(tunnelId)
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
delay(Constants.FOCUS_REQUEST_DELAY)
focusRequester.requestFocus()
}
}
fun saveTrustedSSID() {
if (currentText.isNotEmpty()) {
scope.launch {
optionsViewModel.onSaveRunSSID(currentText).let {
when (it) {
is Result.Success -> currentText = ""
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
}
}
}
}
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier =
Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.clickable(
indication = null,
interactionSource = interactionSource,
) {
focusManager.clearFocus()
},
) {
Surface(
tonalElevation = 2.dp,
shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier =
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier
.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth)
.padding(top = 10.dp)
} else {
Modifier
.fillMaxWidth(fillMaxWidth)
.padding(top = 20.dp)
})
.padding(bottom = 10.dp),
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp),
) {
SectionTitle(
title = stringResource(id = R.string.general),
padding = screenPadding,
)
ConfigurationToggle(
stringResource(R.string.set_primary_tunnel),
enabled = true,
checked = uiState.isDefaultTunnel,
modifier = Modifier
.focusRequester(focusRequester),
padding = screenPadding,
onCheckChanged = { optionsViewModel.onTogglePrimaryTunnel() },
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxSize()
.padding(top = 5.dp),
horizontalArrangement = Arrangement.Center,
) {
TextButton(
onClick = {
navController.navigate(
"${Screen.Config.route}/${tunnelId}",
)
},
) {
Text(stringResource(R.string.edit_tunnel))
}
}
}
}
Surface(
tonalElevation = 2.dp,
shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier =
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier
.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth)
.padding(top = 10.dp)
} else {
Modifier
.fillMaxWidth(fillMaxWidth)
.padding(top = 20.dp)
})
.padding(bottom = 10.dp),
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp),
) {
SectionTitle(
title = stringResource(id = R.string.auto_tunneling),
padding = screenPadding,
)
ConfigurationToggle(
stringResource(R.string.mobile_data_tunnel),
enabled = true,
checked = uiState.tunnel?.isMobileDataTunnel == true,
padding = screenPadding,
onCheckChanged = { optionsViewModel.onToggleIsMobileDataTunnel() },
)
Column {
FlowRow(
modifier = Modifier
.padding(screenPadding)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp),
) {
uiState.tunnel?.tunnelNetworks?.forEach { ssid ->
ClickableIconButton(
onClick = {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
focusRequester.requestFocus()
optionsViewModel.onDeleteRunSSID(ssid)
}
},
onIconClick = {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus()
optionsViewModel.onDeleteRunSSID(ssid)
},
text = ssid,
icon = Icons.Filled.Close,
enabled = true,
)
}
if (uiState.tunnel == null || uiState.tunnel?.tunnelNetworks?.isEmpty() == true) {
Text(
stringResource(R.string.no_wifi_names_configured),
fontStyle = FontStyle.Italic,
color = Color.Gray,
)
}
}
OutlinedTextField(
enabled = true,
value = currentText,
onValueChange = { currentText = it },
label = { Text(stringResource(id = R.string.use_tunnel_on_wifi_name)) },
modifier =
Modifier
.padding(
start = screenPadding,
top = 5.dp,
bottom = 10.dp,
),
maxLines = 1,
keyboardOptions =
KeyboardOptions(
capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
trailingIcon = {
if (currentText != "") {
IconButton(onClick = { saveTrustedSSID() }) {
Icon(
imageVector = Icons.Outlined.Add,
contentDescription =
if (currentText == "") {
stringResource(
id =
R.string
.trusted_ssid_empty_description,
)
} else {
stringResource(
id =
R.string
.trusted_ssid_value_description,
)
},
tint = MaterialTheme.colorScheme.primary,
)
}
}
},
)
}
}
}
}
}

View File

@ -0,0 +1,9 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.options
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
data class OptionsUiState(
val id: String? = null,
val tunnel: TunnelConfig? = null,
val isDefaultTunnel: Boolean = false
)

View File

@ -0,0 +1,101 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.options
import androidx.compose.ui.util.fastFirstOrNull
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.Result
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
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@HiltViewModel
class OptionsViewModel @Inject
constructor(
private val appDataRepository: AppDataRepository
) : ViewModel() {
private val _optionState = MutableStateFlow(OptionsUiState())
val uiState = combine(
appDataRepository.tunnels.getTunnelConfigsFlow(),
_optionState,
) { tunnels, optionState ->
if (optionState.id != null) {
val tunnelConfig = tunnels.fastFirstOrNull { it.id.toString() == optionState.id }
val isPrimaryTunnel = tunnelConfig?.isPrimaryTunnel == true
OptionsUiState(optionState.id, tunnelConfig, isPrimaryTunnel)
} else OptionsUiState()
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
OptionsUiState(),
)
fun init(tunnelId: String) {
_optionState.value = _optionState.value.copy(
id = tunnelId,
)
}
fun onDeleteRunSSID(ssid: String) = viewModelScope.launch(Dispatchers.IO) {
uiState.value.tunnel?.let {
appDataRepository.tunnels.save(
tunnelConfig = it.copy(
tunnelNetworks = (uiState.value.tunnel!!.tunnelNetworks - ssid).toMutableList(),
),
)
}
}
private fun saveTunnel(tunnelConfig: TunnelConfig?) = viewModelScope.launch(Dispatchers.IO) {
tunnelConfig?.let {
appDataRepository.tunnels.save(it)
}
}
suspend fun onSaveRunSSID(ssid: String): Result<Unit> {
val trimmed = ssid.trim()
val tunnelsWithName = withContext(viewModelScope.coroutineContext) {
appDataRepository.tunnels.findByTunnelNetworksName(trimmed)
}
return if (uiState.value.tunnel?.tunnelNetworks?.contains(trimmed) != true &&
tunnelsWithName.isEmpty()) {
uiState.value.tunnel?.tunnelNetworks?.add(trimmed)
saveTunnel(uiState.value.tunnel)
Result.Success(Unit)
} else {
Result.Error(Event.Error.SsidConflict)
}
}
fun onToggleIsMobileDataTunnel() = viewModelScope.launch(Dispatchers.IO) {
uiState.value.tunnel?.let {
if (it.isMobileDataTunnel) {
appDataRepository.tunnels.updateMobileDataTunnel(null)
} else appDataRepository.tunnels.updateMobileDataTunnel(it)
}
}
fun onTogglePrimaryTunnel() = viewModelScope.launch(Dispatchers.IO) {
if (uiState.value.tunnel != null) {
appDataRepository.tunnels.updatePrimaryTunnel(
when (uiState.value.isDefaultTunnel) {
true -> null
false -> uiState.value.tunnel
},
)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance)
}
}
}

View File

@ -27,11 +27,11 @@ fun PinLockScreen(navController: NavController, appViewModel: AppViewModel) {
color = MaterialTheme.colorScheme.surface,
onPinCorrect = {
// pin is correct, navigate or hide pin lock
if(WireGuardAutoTunnel.isRunningOnAndroidTv()) {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
navController.navigate(Screen.Main.route)
} else {
val isPopped = navController.popBackStack()
if(!isPopped) {
if (!isPopped) {
navController.navigate(Screen.Main.route)
}
}
@ -39,11 +39,15 @@ fun PinLockScreen(navController: NavController, appViewModel: AppViewModel) {
},
onPinIncorrect = {
// pin is incorrect, show error
appViewModel.showSnackbarMessage(StringValue.StringResource(R.string.incorrect_pin).asString(context))
appViewModel.showSnackbarMessage(
StringValue.StringResource(R.string.incorrect_pin).asString(context),
)
},
onPinCreated = {
// pin created for the first time, navigate or hide pin lock
appViewModel.showSnackbarMessage(StringValue.StringResource(R.string.pin_created).asString(context))
appViewModel.showSnackbarMessage(
StringValue.StringResource(R.string.pin_created).asString(context),
)
},
)
}

View File

@ -160,7 +160,7 @@ fun SettingsScreen(
fun handleAutoTunnelToggle() {
if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) {
if (appViewModel.isRequiredPermissionGranted()) {
viewModel.toggleAutoTunnel()
viewModel.onToggleAutoTunnel()
}
} else {
requestBatteryOptimizationsDisabled()
@ -247,15 +247,15 @@ fun SettingsScreen(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState),
.fillMaxSize()
.verticalScroll(scrollState),
) {
Icon(
Icons.Rounded.LocationOff,
contentDescription = stringResource(id = R.string.map),
modifier = Modifier
.padding(30.dp)
.size(128.dp),
.padding(30.dp)
.size(128.dp),
)
Text(
stringResource(R.string.prominent_background_location_title),
@ -273,12 +273,12 @@ fun SettingsScreen(
modifier =
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier
.fillMaxWidth()
.padding(10.dp)
.fillMaxWidth()
.padding(10.dp)
} else {
Modifier
.fillMaxWidth()
.padding(30.dp)
.fillMaxWidth()
.padding(30.dp)
},
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly,
@ -336,14 +336,14 @@ fun SettingsScreen(
verticalArrangement = Arrangement.Top,
modifier =
Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.clickable(
indication = null,
interactionSource = interactionSource,
) {
focusManager.clearFocus()
},
.fillMaxSize()
.verticalScroll(scrollState)
.clickable(
indication = null,
interactionSource = interactionSource,
) {
focusManager.clearFocus()
},
) {
Surface(
tonalElevation = 2.dp,
@ -353,13 +353,13 @@ fun SettingsScreen(
modifier =
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier
.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth)
.padding(top = 10.dp)
.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth)
.padding(top = 10.dp)
} else {
Modifier
.fillMaxWidth(fillMaxWidth)
.padding(top = 20.dp)
.fillMaxWidth(fillMaxWidth)
.padding(top = 20.dp)
})
.padding(bottom = 10.dp),
) {
@ -390,8 +390,8 @@ fun SettingsScreen(
Column {
FlowRow(
modifier = Modifier
.padding(screenPadding)
.fillMaxWidth(),
.padding(screenPadding)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp),
) {
uiState.settings.trustedNetworkSSIDs.forEach { ssid ->
@ -503,10 +503,10 @@ fun SettingsScreen(
(if (!uiState.settings.isAutoTunnelEnabled) Modifier
else
Modifier.focusRequester(
focusRequester,
focusRequester,
))
.fillMaxSize()
.padding(top = 5.dp),
.fillMaxSize()
.padding(top = 5.dp),
horizontalArrangement = Arrangement.Center,
) {
TextButton(
@ -557,8 +557,8 @@ fun SettingsScreen(
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier = Modifier
.fillMaxWidth(fillMaxWidth)
.padding(vertical = 10.dp),
.fillMaxWidth(fillMaxWidth)
.padding(vertical = 10.dp),
) {
Column(
horizontalAlignment = Alignment.Start,
@ -596,9 +596,9 @@ fun SettingsScreen(
color = MaterialTheme.colorScheme.surface,
modifier =
Modifier
.fillMaxWidth(fillMaxWidth)
.padding(vertical = 10.dp)
.padding(bottom = 140.dp),
.fillMaxWidth(fillMaxWidth)
.padding(vertical = 10.dp)
.padding(bottom = 140.dp),
) {
Column(
horizontalAlignment = Alignment.Start,
@ -609,7 +609,7 @@ fun SettingsScreen(
title = stringResource(id = R.string.other),
padding = screenPadding,
)
if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
ConfigurationToggle(
stringResource(R.string.always_on_vpn_support),
enabled = !uiState.settings.isAutoTunnelEnabled,
@ -639,7 +639,7 @@ fun SettingsScreen(
}
},
)
if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier

View File

@ -7,10 +7,9 @@ import androidx.core.location.LocationManagerCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wireguard.android.util.RootShell
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.util.Constants
@ -29,28 +28,27 @@ class SettingsViewModel
@Inject
constructor(
private val application: Application,
private val tunnelConfigRepository: TunnelConfigRepository,
private val settingsRepository: SettingsRepository,
private val dataStoreManager: DataStoreManager,
private val appDataRepository: AppDataRepository,
private val serviceManager: ServiceManager,
private val rootShell: RootShell,
private val vpnService: VpnService
vpnService: VpnService
) : ViewModel() {
val uiState =
combine(
settingsRepository.getSettingsFlow(),
tunnelConfigRepository.getTunnelConfigsFlow(),
vpnService.vpnState,
dataStoreManager.preferencesFlow,
) { settings, tunnels, tunnelState, preferences ->
SettingsUiState(
settings,
tunnels,
tunnelState,
preferences?.get(DataStoreManager.LOCATION_DISCLOSURE_SHOWN) ?: false,
preferences?.get(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN) ?: false,
)
}
appDataRepository.settings.getSettingsFlow(),
appDataRepository.tunnels.getTunnelConfigsFlow(),
vpnService.vpnState,
appDataRepository.appState.generalStateFlow,
) { settings, tunnels, tunnelState, generalState ->
SettingsUiState(
settings,
tunnels,
tunnelState,
generalState.locationDisclosureShown,
generalState.batteryOptimizationDisableShown,
)
}
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
@ -70,12 +68,12 @@ constructor(
fun setLocationDisclosureShown() =
viewModelScope.launch {
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, true)
appDataRepository.appState.setLocationDisclosureShown(true)
}
fun setBatteryOptimizeDisableShown() =
viewModelScope.launch {
dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, true)
appDataRepository.appState.setBatteryOptimizationDisableShown(true)
}
fun onToggleTunnelOnMobileData() {
@ -90,48 +88,42 @@ constructor(
saveSettings(
uiState.value.settings.copy(
trustedNetworkSSIDs =
(uiState.value.settings.trustedNetworkSSIDs - ssid).toMutableList(),
(uiState.value.settings.trustedNetworkSSIDs - ssid).toMutableList(),
),
)
}
private suspend fun getDefaultTunnelOrFirst(): String {
return uiState.value.settings.defaultTunnel
?: tunnelConfigRepository.getAll().first().toString()
}
fun toggleAutoTunnel() =
fun onToggleAutoTunnel() =
viewModelScope.launch {
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused
if (isAutoTunnelEnabled) {
ServiceManager.stopWatcherService(application)
serviceManager.stopWatcherService(application)
} else {
ServiceManager.startWatcherService(application)
serviceManager.startWatcherService(application)
isAutoTunnelPaused = false
}
saveSettings(
uiState.value.settings.copy(
isAutoTunnelEnabled = !isAutoTunnelEnabled,
isAutoTunnelPaused = isAutoTunnelPaused,
defaultTunnel = getDefaultTunnelOrFirst(),
),
)
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
}
fun onToggleAlwaysOnVPN() =
viewModelScope.launch {
val updatedSettings =
saveSettings(
uiState.value.settings.copy(
isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled,
defaultTunnel = getDefaultTunnelOrFirst(),
)
saveSettings(updatedSettings)
),
)
}
private fun saveSettings(settings: Settings) =
viewModelScope.launch { settingsRepository.save(settings) }
viewModelScope.launch { appDataRepository.settings.save(settings) }
fun onToggleTunnelOnEthernet() {
saveSettings(
@ -154,14 +146,6 @@ constructor(
)
}
fun onToggleBatterySaver() {
saveSettings(
uiState.value.settings.copy(
isBatterySaverEnabled = !uiState.value.settings.isBatterySaverEnabled,
),
)
}
private fun saveKernelMode(on: Boolean) {
saveSettings(
uiState.value.settings.copy(

View File

@ -50,7 +50,6 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
@Composable
fun SupportScreen(
@ -71,7 +70,7 @@ fun SupportScreen(
Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.focusable()
.focusable(),
) {
Surface(
tonalElevation = 2.dp,
@ -79,17 +78,17 @@ fun SupportScreen(
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier =
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier
.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth)
.padding(top = 10.dp)
} else {
Modifier
.fillMaxWidth(fillMaxWidth)
.padding(top = 20.dp)
})
.padding(bottom = 25.dp),
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier
.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth)
.padding(top = 10.dp)
} else {
Modifier
.fillMaxWidth(fillMaxWidth)
.padding(top = 20.dp)
})
.padding(bottom = 25.dp),
) {
Column(modifier = Modifier.padding(20.dp)) {
val forwardIcon = Icons.AutoMirrored.Rounded.ArrowForward
@ -128,13 +127,13 @@ fun SupportScreen(
}
Icon(
forwardIcon,
forwardIcon.name
forwardIcon.name,
)
}
}
HorizontalDivider(
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.onBackground
color = MaterialTheme.colorScheme.onBackground,
)
TextButton(
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.discord_url)) },
@ -160,13 +159,13 @@ fun SupportScreen(
}
Icon(
forwardIcon,
forwardIcon.name
forwardIcon.name,
)
}
}
HorizontalDivider(
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.onBackground
color = MaterialTheme.colorScheme.onBackground,
)
TextButton(
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.github_url)) },
@ -192,13 +191,13 @@ fun SupportScreen(
}
Icon(
forwardIcon,
forwardIcon.name
forwardIcon.name,
)
}
}
HorizontalDivider(
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.onBackground
color = MaterialTheme.colorScheme.onBackground,
)
TextButton(
onClick = { appViewModel.launchEmail() },
@ -220,14 +219,14 @@ fun SupportScreen(
}
Icon(
forwardIcon,
forwardIcon.name
forwardIcon.name,
)
}
}
if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
HorizontalDivider(
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.onBackground
color = MaterialTheme.colorScheme.onBackground,
)
TextButton(
onClick = { navController.navigate(Screen.Support.Logs.route) },
@ -249,7 +248,7 @@ fun SupportScreen(
}
Icon(
Icons.AutoMirrored.Rounded.ArrowForward,
stringResource(id = R.string.go)
stringResource(id = R.string.go),
)
}
}
@ -262,9 +261,9 @@ fun SupportScreen(
style = TextStyle(textDecoration = TextDecoration.Underline),
fontSize = 16.sp,
modifier =
Modifier.clickable {
appViewModel.openWebPage(context.resources.getString(R.string.privacy_policy_url))
},
Modifier.clickable {
appViewModel.openWebPage(context.resources.getString(R.string.privacy_policy_url))
},
)
Row(
horizontalArrangement = Arrangement.spacedBy(25.dp),

View File

@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
@HiltViewModel
class SupportViewModel @Inject constructor(private val settingsRepository: SettingsRepository) :
class SupportViewModel @Inject constructor(settingsRepository: SettingsRepository) :
ViewModel() {
val uiState =

View File

@ -31,7 +31,6 @@ 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 androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel
import kotlinx.coroutines.launch
@ -48,7 +47,7 @@ fun LogsScreen(appViewModel: AppViewModel) {
val clipboardManager: ClipboardManager = LocalClipboardManager.current
val scope = rememberCoroutineScope()
LaunchedEffect(logs.size){
LaunchedEffect(logs.size) {
scope.launch {
lazyColumnListState.animateScrollToItem(logs.size)
}
@ -61,16 +60,16 @@ fun LogsScreen(appViewModel: AppViewModel) {
appViewModel.saveLogsToFile()
},
shape = RoundedCornerShape(16.dp),
containerColor = MaterialTheme.colorScheme.primary
containerColor = MaterialTheme.colorScheme.primary,
) {
val icon = Icons.Filled.Save
Icon(
imageVector = icon,
contentDescription = icon.name,
tint = MaterialTheme.colorScheme.onPrimary
tint = MaterialTheme.colorScheme.onPrimary,
)
}
}
},
) {
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
@ -78,9 +77,12 @@ fun LogsScreen(appViewModel: AppViewModel) {
state = lazyColumnListState,
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 24.dp)) {
.padding(horizontal = 24.dp),
) {
items(logs) {
Row(horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start), verticalAlignment = Alignment.Top,
Row(
horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start),
verticalAlignment = Alignment.Top,
modifier = Modifier
.fillMaxSize()
.clickable(
@ -88,13 +90,17 @@ fun LogsScreen(appViewModel: AppViewModel) {
indication = null,
onClick = {
clipboardManager.setText(annotatedString = AnnotatedString(it.toString()))
}
)
},
),
) {
val fontSize = 10.sp
Text(text = it.tag, modifier = Modifier.fillMaxSize(0.3f), fontSize = fontSize)
LogTypeLabel(color = Color(it.level.color())) {
Text(text = it.level.signifier, textAlign = TextAlign.Center, fontSize = fontSize)
Text(
text = it.level.signifier,
textAlign = TextAlign.Center,
fontSize = fontSize,
)
}
Text("${it.message} - ${it.time}", fontSize = fontSize)
}

View File

@ -57,6 +57,7 @@ fun WireguardAutoTunnelTheme(
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}

View File

@ -10,13 +10,13 @@ import androidx.compose.ui.unit.sp
val Typography =
Typography(
bodyLarge =
TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp,
),
TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp,
),
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,

View File

@ -10,6 +10,7 @@ object Constants {
const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L
const val VPN_CONNECTED_NOTIFICATION_DELAY = 3_000L
const val TOGGLE_TUNNEL_DELAY = 300L
const val WATCHER_COLLECTION_DELAY = 1_000L
const val CONF_FILE_EXTENSION = ".conf"
const val ZIP_FILE_EXTENSION = ".zip"
const val URI_CONTENT_SCHEME = "content"
@ -17,6 +18,7 @@ object Constants {
const val TEXT_MIME_TYPE = "text/plain"
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 SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024
@ -29,4 +31,8 @@ object Constants {
const val PING_INTERVAL = 60_000L
const val PING_COOLDOWN = PING_INTERVAL * 60 //one hour
const val ALLOWED_DISPLAY_NAME_LENGTH = 20
const val TUNNEL_EXTRA_KEY = "tunnelId"
}

View File

@ -18,7 +18,7 @@ sealed class Event {
get() = WireGuardAutoTunnel.instance.getString(R.string.error_ssid_exists)
}
data class ConfigParseError(val appendedMessage : String) : Error() {
data class ConfigParseError(val appendedMessage: String) : Error() {
override val message: String =
WireGuardAutoTunnel.instance.getString(R.string.config_parse_error) + (
if (appendedMessage != "") ": ${appendedMessage.trim()}" else "")

View File

@ -31,8 +31,8 @@ fun BroadcastReceiver.goAsync(
}
}
fun String.truncateWithEllipsis(allowedLength : Int) : String {
return if(this.length > allowedLength + 3) {
fun String.truncateWithEllipsis(allowedLength: Int): String {
return if (this.length > allowedLength + 3) {
this.substring(0, allowedLength) + "***"
} else this
}

View File

@ -16,6 +16,7 @@ import java.util.zip.ZipOutputStream
object FileUtils {
private const val ZIP_FILE_MIME_TYPE = "application/zip"
//TODO issue with android 9
private fun createDownloadsFileOutputStream(
context: Context,
fileName: String,
@ -45,11 +46,11 @@ object FileUtils {
}
fun saveFileToDownloads(context: Context, content: String, fileName: String) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
put(MediaStore.MediaColumns.MIME_TYPE, Constants.TEXT_MIME_TYPE)
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
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)
@ -61,7 +62,7 @@ object FileUtils {
} else {
val target = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
fileName
fileName,
)
FileOutputStream(target).use { output ->
output.write(content.toByteArray())

View File

@ -22,11 +22,11 @@ object NumberUtils {
return "tunnel${randomFive()}"
}
private fun randomFive() : Int {
private fun randomFive(): Int {
return (Math.random() * 100000).toInt()
}
fun randomThree() : Int {
fun randomThree(): Int {
return (Math.random() * 1000).toInt()
}

View File

@ -1,13 +1,17 @@
package com.zaneschepke.wireguardautotunnel.util
import android.util.Log
import timber.log.Timber
class ReleaseTree : Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
when(priority) {
Log.DEBUG -> return
}
super.log(priority,tag,message,t)
class ReleaseTree : Timber.DebugTree() {
override fun d(t: Throwable?) {
return
}
override fun d(t: Throwable?, message: String?, vararg args: Any?) {
return
}
override fun d(message: String?, vararg args: Any?) {
return
}
}

View File

@ -1,17 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="640"
android:viewportHeight="640"
android:tint="#FFFFFF">
<group android:scaleX="1.0132159"
android:scaleY="1.0132159"
android:translateX="-4.229075"
android:translateY="-4.229075">
<path
android:pathData="M316.72,80.15C314.94,80.82 312.08,83.95 308.79,88.84C275.66,138.15 157.88,161.96 119.66,127.08C109.97,118.24 101.21,118.84 98.97,128.5C96.01,141.29 98.49,204.07 103.12,233.5C123.71,364.32 186.77,465.69 303.03,554.88C314.06,563.34 316.63,563.42 326.93,555.61C329.91,553.35 336.21,548.63 340.93,545.13C345.64,541.63 350.87,537.46 352.55,535.88C354.23,534.29 357.92,531.53 360.75,529.74C413.56,496.43 481.74,399.04 510.38,316C527.22,267.19 534.86,236.66 539.96,197.9C547.99,136.74 545.31,124.46 526,134.01C469.85,161.74 361.02,137.16 333.39,90.49C327.63,80.75 322.93,77.84 316.72,80.15M307.5,195.34C282.24,203.76 266.16,237.38 269.85,274.04C270.99,285.39 271.18,285 264.63,285C254.13,285 254.15,284.87 255,352.05C255.64,402.94 250.1,397.02 298.5,398.52C352.54,400.2 377.63,400.23 379.23,398.63C381.67,396.18 381.86,392.86 382.46,339.89C383.09,283.93 383.39,286 374.51,286C367.07,286 367.21,286.26 367.77,273.58C369.89,225.38 338.74,184.92 307.5,195.34M308.93,216.98C294.31,224.7 281.68,270.05 290.33,283.75C291.74,285.99 346.85,285.51 347.78,283.25C359.48,254.75 330.62,205.51 308.93,216.98M309.54,317.1C304.18,321.81 304.39,327.76 310.11,332.99C314.78,337.26 314.82,336.51 309.33,349.89C307.44,354.51 306.13,358.89 306.42,359.64C306.96,361.06 329,361.75 329,360.35C329,359.98 327.42,354.82 325.5,348.86C323.58,342.91 322,337.52 322,336.89C322,336.26 323.58,334 325.5,331.87C335.69,320.59 321.02,307.02 309.54,317.1"
android:fillColor="#53bdb6"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
</group>
android:viewportHeight="640">
<group
android:scaleX="1.0132159"
android:scaleY="1.0132159"
android:translateX="-4.229075"
android:translateY="-4.229075">
<path
android:fillColor="#53bdb6"
android:fillType="evenOdd"
android:pathData="M316.72,80.15C314.94,80.82 312.08,83.95 308.79,88.84C275.66,138.15 157.88,161.96 119.66,127.08C109.97,118.24 101.21,118.84 98.97,128.5C96.01,141.29 98.49,204.07 103.12,233.5C123.71,364.32 186.77,465.69 303.03,554.88C314.06,563.34 316.63,563.42 326.93,555.61C329.91,553.35 336.21,548.63 340.93,545.13C345.64,541.63 350.87,537.46 352.55,535.88C354.23,534.29 357.92,531.53 360.75,529.74C413.56,496.43 481.74,399.04 510.38,316C527.22,267.19 534.86,236.66 539.96,197.9C547.99,136.74 545.31,124.46 526,134.01C469.85,161.74 361.02,137.16 333.39,90.49C327.63,80.75 322.93,77.84 316.72,80.15M307.5,195.34C282.24,203.76 266.16,237.38 269.85,274.04C270.99,285.39 271.18,285 264.63,285C254.13,285 254.15,284.87 255,352.05C255.64,402.94 250.1,397.02 298.5,398.52C352.54,400.2 377.63,400.23 379.23,398.63C381.67,396.18 381.86,392.86 382.46,339.89C383.09,283.93 383.39,286 374.51,286C367.07,286 367.21,286.26 367.77,273.58C369.89,225.38 338.74,184.92 307.5,195.34M308.93,216.98C294.31,224.7 281.68,270.05 290.33,283.75C291.74,285.99 346.85,285.51 347.78,283.25C359.48,254.75 330.62,205.51 308.93,216.98M309.54,317.1C304.18,321.81 304.39,327.76 310.11,332.99C314.78,337.26 314.82,336.51 309.33,349.89C307.44,354.51 306.13,358.89 306.42,359.64C306.96,361.06 329,361.75 329,360.35C329,359.98 327.42,354.82 325.5,348.86C323.58,342.91 322,337.52 322,336.89C322,336.26 323.58,334 325.5,331.87C335.69,320.59 321.02,307.02 309.54,317.1"
android:strokeColor="#00000000" />
</group>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#FF000000"
android:pathData="M360,600v-240h80v240h-80ZM520,600v-240h80v240h-80ZM480,920q-108,0 -202.5,-49.5T120,732v108L40,840v-240h240v80h-98q51,75 129.5,117.5T480,840q115,0 208.5,-66T820,599l78,18q-45,136 -160,219.5T480,920ZM42,440q7,-67 32,-128.5T143,198l57,57q-32,41 -52,87.5T123,440L42,440ZM256,199 L199,142q53,-44 114,-69.5T440,42v80q-51,5 -97,25t-87,52ZM705,199q-41,-32 -87.5,-52T520,122v-80q67,6 128.5,31T762,142l-57,57ZM838,440q-5,-51 -25,-97.5T761,255l57,-57q44,52 69,113.5T918,440h-80Z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#FF000000"
android:pathData="M380,660v-360l280,180 -280,180ZM480,920q-108,0 -202.5,-49.5T120,732v108L40,840v-240h240v80h-98q51,75 129.5,117.5T480,840q115,0 208.5,-66T820,599l78,18q-45,136 -160,219.5T480,920ZM42,440q7,-67 32,-128.5T143,198l57,57q-32,41 -52,87.5T123,440L42,440ZM256,199 L199,142q53,-44 114,-69.5T440,42v80q-51,5 -97,25t-87,52ZM705,199q-41,-32 -87.5,-52T520,122v-80q67,6 128.5,31T762,142l-57,57ZM838,440q-5,-51 -25,-97.5T761,255l57,-57q44,52 69,113.5T918,440h-80Z" />
</vector>

View File

@ -3,43 +3,54 @@
android:height="180dp"
android:viewportWidth="640"
android:viewportHeight="640">
<group android:scaleX="0.6666667"
android:scaleY="0.6666667"
android:translateX="106.666664"
android:translateY="106.666664">
<group android:scaleX="0.315"
android:scaleY="0.56"
android:translateX="46.4"
android:translateY="140.8">
<path
android:pathData="M316.72,80.15C314.94,80.82 312.08,83.95 308.79,88.84C275.66,138.15 157.88,161.96 119.66,127.08C109.97,118.24 101.21,118.84 98.97,128.5C96.01,141.29 98.49,204.07 103.12,233.5C123.71,364.32 186.77,465.69 303.03,554.88C314.06,563.34 316.63,563.42 326.93,555.61C329.91,553.35 336.21,548.63 340.93,545.13C345.64,541.63 350.87,537.46 352.55,535.88C354.23,534.29 357.92,531.53 360.75,529.74C413.56,496.43 481.74,399.04 510.38,316C527.22,267.19 534.86,236.66 539.96,197.9C547.99,136.74 545.31,124.46 526,134.01C469.85,161.74 361.02,137.16 333.39,90.49C327.63,80.75 322.93,77.84 316.72,80.15M307.5,195.34C282.24,203.76 266.16,237.38 269.85,274.04C270.99,285.39 271.18,285 264.63,285C254.13,285 254.15,284.87 255,352.05C255.64,402.94 250.1,397.02 298.5,398.52C352.54,400.2 377.63,400.23 379.23,398.63C381.67,396.18 381.86,392.86 382.46,339.89C383.09,283.93 383.39,286 374.51,286C367.07,286 367.21,286.26 367.77,273.58C369.89,225.38 338.74,184.92 307.5,195.34M308.93,216.98C294.31,224.7 281.68,270.05 290.33,283.75C291.74,285.99 346.85,285.51 347.78,283.25C359.48,254.75 330.62,205.51 308.93,216.98M309.54,317.1C304.18,321.81 304.39,327.76 310.11,332.99C314.78,337.26 314.82,336.51 309.33,349.89C307.44,354.51 306.13,358.89 306.42,359.64C306.96,361.06 329,361.75 329,360.35C329,359.98 327.42,354.82 325.5,348.86C323.58,342.91 322,337.52 322,336.89C322,336.26 323.58,334 325.5,331.87C335.69,320.59 321.02,307.02 309.54,317.1"
android:fillColor="#53bdb6"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
</group>
<group
android:scaleX="0.6666667"
android:scaleY="0.6666667"
android:translateX="106.666664"
android:translateY="106.666664">
<group
android:scaleX="0.315"
android:scaleY="0.56"
android:translateX="46.4"
android:translateY="140.8">
<path
android:fillColor="#53bdb6"
android:fillType="evenOdd"
android:pathData="M316.72,80.15C314.94,80.82 312.08,83.95 308.79,88.84C275.66,138.15 157.88,161.96 119.66,127.08C109.97,118.24 101.21,118.84 98.97,128.5C96.01,141.29 98.49,204.07 103.12,233.5C123.71,364.32 186.77,465.69 303.03,554.88C314.06,563.34 316.63,563.42 326.93,555.61C329.91,553.35 336.21,548.63 340.93,545.13C345.64,541.63 350.87,537.46 352.55,535.88C354.23,534.29 357.92,531.53 360.75,529.74C413.56,496.43 481.74,399.04 510.38,316C527.22,267.19 534.86,236.66 539.96,197.9C547.99,136.74 545.31,124.46 526,134.01C469.85,161.74 361.02,137.16 333.39,90.49C327.63,80.75 322.93,77.84 316.72,80.15M307.5,195.34C282.24,203.76 266.16,237.38 269.85,274.04C270.99,285.39 271.18,285 264.63,285C254.13,285 254.15,284.87 255,352.05C255.64,402.94 250.1,397.02 298.5,398.52C352.54,400.2 377.63,400.23 379.23,398.63C381.67,396.18 381.86,392.86 382.46,339.89C383.09,283.93 383.39,286 374.51,286C367.07,286 367.21,286.26 367.77,273.58C369.89,225.38 338.74,184.92 307.5,195.34M308.93,216.98C294.31,224.7 281.68,270.05 290.33,283.75C291.74,285.99 346.85,285.51 347.78,283.25C359.48,254.75 330.62,205.51 308.93,216.98M309.54,317.1C304.18,321.81 304.39,327.76 310.11,332.99C314.78,337.26 314.82,336.51 309.33,349.89C307.44,354.51 306.13,358.89 306.42,359.64C306.96,361.06 329,361.75 329,360.35C329,359.98 327.42,354.82 325.5,348.86C323.58,342.91 322,337.52 322,336.89C322,336.26 323.58,334 325.5,331.87C335.69,320.59 321.02,307.02 309.54,317.1"
android:strokeColor="#00000000" />
</group>
<group android:scaleX="0.52644"
android:scaleY="0.20707752"
android:translateX="320.4"
android:translateY="70.26513">
<group android:translateY="132.92308">
<path android:pathData="M76.06154,-101.68616L87.95077,-101.68616L72.44308,0L55.606155,0L43.93846,-76.72615L32.196922,0L14.990769,0L0.6646154,-101.68616L13.513846,-101.68616L24.36923,-14.104615L36.553844,-89.870766L52.283077,-89.870766L63.876923,-14.104615Z"
android:fillColor="#53BDB6"/>
<path android:pathData="M135.30154,1.6984615Q123.11692,1.6984615,114.25539,-4.1723075Q105.393845,-10.0430765,100.55692,-21.821539Q95.72,-33.6,95.72,-50.88Q95.72,-67.643074,101.62769,-79.495384Q107.535385,-91.347694,117.246155,-97.32923Q126.956924,-103.31077,138.25539,-103.31077Q147.78154,-103.31077,154.42769,-100.61539Q161.07385,-97.92,167.35077,-92.16L159.59692,-84.11077Q154.94461,-88.46769,149.77539,-90.535385Q144.60616,-92.60307,138.32922,-92.60307Q130.42769,-92.60307,123.92923,-88.504616Q117.43077,-84.40615,113.44308,-75.10154Q109.45538,-65.79692,109.45538,-50.88Q109.45538,-29.095385,116.175385,-19.089231Q122.895386,-9.0830765,136.48308,-9.0830765Q146.7477,-9.0830765,155.83076,-13.9569235L155.83076,-44.97231L135.81847,-44.97231L134.26768,-55.606155L168.5323,-55.606155L168.5323,-7.163077Q160.70462,-2.88,152.98769,-0.59076923Q145.27077,1.6984615,135.30154,1.6984615Z"
android:fillColor="#53BDB6"/>
<path android:pathData="M348.30463,-90.683075L317.43692,-90.683075L317.43692,0L304.66153,0L304.66153,-90.683075L272.90768,-90.683075L272.90768,-101.68616L349.63385,-101.68616Z"
android:fillColor="#53BDB6"/>
<path android:pathData="M383.24924,-22.670769Q383.24924,-15.064615,386.3877,-11.630769Q389.52615,-8.196923,396.32,-8.196923Q402.44922,-8.196923,408.24615,-11.741538Q414.0431,-15.286154,417.36615,-20.52923L417.36615,-77.76L429.7723,-77.76L429.7723,0L419.2123,0L418.17847,-10.486154Q413.67386,-4.726154,406.99078,-1.5138462Q400.30768,1.6984615,393.2923,1.6984615Q382.14154,1.6984615,376.4923,-4.246154Q370.84308,-10.190769,370.84308,-21.193846L370.84308,-77.76L383.24924,-77.76Z"
android:fillColor="#53BDB6"/>
<path android:pathData="M459.84308,0L459.84308,-77.76L470.40308,-77.76L471.36307,-66.97846Q476.01538,-72.81231,483.0677,-76.098465Q490.12,-79.38461,497.06152,-79.38461Q508.2123,-79.38461,513.56616,-73.47692Q518.92,-67.56923,518.92,-56.49231L518.92,0L506.51385,0L506.51385,-47.335384Q506.51385,-56.049232,505.59076,-60.627693Q504.6677,-65.206154,501.82462,-67.42154Q498.98154,-69.636925,493.22153,-69.636925Q486.9446,-69.636925,481.36923,-65.76Q475.79385,-61.883076,472.24924,-56.64L472.24924,0Z"
android:fillColor="#53BDB6"/>
<path android:pathData="M548.8431,0L548.8431,-77.76L559.4031,-77.76L560.3631,-66.97846Q565.0154,-72.81231,572.0677,-76.098465Q579.12,-79.38461,586.0615,-79.38461Q597.2123,-79.38461,602.56616,-73.47692Q607.92,-67.56923,607.92,-56.49231L607.92,0L595.51385,0L595.51385,-47.335384Q595.51385,-56.049232,594.59076,-60.627693Q593.66766,-65.206154,590.8246,-67.42154Q587.98157,-69.636925,582.22156,-69.636925Q575.94464,-69.636925,570.3692,-65.76Q564.7938,-61.883076,561.2492,-56.64L561.2492,0Z"
android:fillColor="#53BDB6"/>
<path android:pathData="M647.59076,-34.486153Q647.88617,-25.772308,650.9877,-19.975384Q654.08923,-14.178461,659.1108,-11.409231Q664.1323,-8.64,670.3354,-8.64Q676.0954,-8.64,680.9323,-10.338462Q685.7692,-12.036923,691.0862,-15.655385L696.92,-7.4584618Q691.4554,-3.1753845,684.4031,-0.73846155Q677.35077,1.6984615,670.1877,1.6984615Q659.0369,1.6984615,650.9877,-3.323077Q642.9385,-8.344615,638.7662,-17.50154Q634.5939,-26.65846,634.5939,-38.76923Q634.5939,-50.51077,638.8031,-59.74154Q643.0123,-68.972305,650.6923,-74.17846Q658.3723,-79.38461,668.56305,-79.38461Q678.3108,-79.38461,685.43695,-74.80615Q692.56305,-70.22769,696.36615,-61.624615Q700.16925,-53.021538,700.16925,-41.28Q700.16925,-37.883076,699.87384,-34.486153ZM668.71075,-69.19385Q659.70154,-69.19385,654.0523,-62.88Q648.4031,-56.566154,647.6646,-44.086155L688.2062,-44.086155Q687.9846,-56.344616,682.81537,-62.76923Q677.6462,-69.19385,668.71075,-69.19385Z"
android:fillColor="#53BDB6"/>
<path android:pathData="M757.12,-19.2Q757.12,-13.735385,760.55383,-11.187693Q763.9877,-8.64,769.96924,-8.64Q776.24615,-8.64,783.04,-11.372308L786.3631,-2.2892308Q782.8185,-0.51692307,778.0923,0.59076923Q773.36615,1.6984615,767.90155,1.6984615Q761.0339,1.6984615,755.75385,-0.8861538Q750.4738,-3.4707692,747.5939,-8.344615Q744.71387,-13.218462,744.71387,-19.864614L744.71387,-99.24923L720.8615,-99.24923L720.8615,-109.07077L757.12,-109.07077Z"
android:fillColor="#53BDB6"/>
</group>
<group
android:scaleX="0.52644"
android:scaleY="0.20707752"
android:translateX="320.4"
android:translateY="70.26513">
<group android:translateY="132.92308">
<path
android:fillColor="#53BDB6"
android:pathData="M76.06154,-101.68616L87.95077,-101.68616L72.44308,0L55.606155,0L43.93846,-76.72615L32.196922,0L14.990769,0L0.6646154,-101.68616L13.513846,-101.68616L24.36923,-14.104615L36.553844,-89.870766L52.283077,-89.870766L63.876923,-14.104615Z" />
<path
android:fillColor="#53BDB6"
android:pathData="M135.30154,1.6984615Q123.11692,1.6984615,114.25539,-4.1723075Q105.393845,-10.0430765,100.55692,-21.821539Q95.72,-33.6,95.72,-50.88Q95.72,-67.643074,101.62769,-79.495384Q107.535385,-91.347694,117.246155,-97.32923Q126.956924,-103.31077,138.25539,-103.31077Q147.78154,-103.31077,154.42769,-100.61539Q161.07385,-97.92,167.35077,-92.16L159.59692,-84.11077Q154.94461,-88.46769,149.77539,-90.535385Q144.60616,-92.60307,138.32922,-92.60307Q130.42769,-92.60307,123.92923,-88.504616Q117.43077,-84.40615,113.44308,-75.10154Q109.45538,-65.79692,109.45538,-50.88Q109.45538,-29.095385,116.175385,-19.089231Q122.895386,-9.0830765,136.48308,-9.0830765Q146.7477,-9.0830765,155.83076,-13.9569235L155.83076,-44.97231L135.81847,-44.97231L134.26768,-55.606155L168.5323,-55.606155L168.5323,-7.163077Q160.70462,-2.88,152.98769,-0.59076923Q145.27077,1.6984615,135.30154,1.6984615Z" />
<path
android:fillColor="#53BDB6"
android:pathData="M348.30463,-90.683075L317.43692,-90.683075L317.43692,0L304.66153,0L304.66153,-90.683075L272.90768,-90.683075L272.90768,-101.68616L349.63385,-101.68616Z" />
<path
android:fillColor="#53BDB6"
android:pathData="M383.24924,-22.670769Q383.24924,-15.064615,386.3877,-11.630769Q389.52615,-8.196923,396.32,-8.196923Q402.44922,-8.196923,408.24615,-11.741538Q414.0431,-15.286154,417.36615,-20.52923L417.36615,-77.76L429.7723,-77.76L429.7723,0L419.2123,0L418.17847,-10.486154Q413.67386,-4.726154,406.99078,-1.5138462Q400.30768,1.6984615,393.2923,1.6984615Q382.14154,1.6984615,376.4923,-4.246154Q370.84308,-10.190769,370.84308,-21.193846L370.84308,-77.76L383.24924,-77.76Z" />
<path
android:fillColor="#53BDB6"
android:pathData="M459.84308,0L459.84308,-77.76L470.40308,-77.76L471.36307,-66.97846Q476.01538,-72.81231,483.0677,-76.098465Q490.12,-79.38461,497.06152,-79.38461Q508.2123,-79.38461,513.56616,-73.47692Q518.92,-67.56923,518.92,-56.49231L518.92,0L506.51385,0L506.51385,-47.335384Q506.51385,-56.049232,505.59076,-60.627693Q504.6677,-65.206154,501.82462,-67.42154Q498.98154,-69.636925,493.22153,-69.636925Q486.9446,-69.636925,481.36923,-65.76Q475.79385,-61.883076,472.24924,-56.64L472.24924,0Z" />
<path
android:fillColor="#53BDB6"
android:pathData="M548.8431,0L548.8431,-77.76L559.4031,-77.76L560.3631,-66.97846Q565.0154,-72.81231,572.0677,-76.098465Q579.12,-79.38461,586.0615,-79.38461Q597.2123,-79.38461,602.56616,-73.47692Q607.92,-67.56923,607.92,-56.49231L607.92,0L595.51385,0L595.51385,-47.335384Q595.51385,-56.049232,594.59076,-60.627693Q593.66766,-65.206154,590.8246,-67.42154Q587.98157,-69.636925,582.22156,-69.636925Q575.94464,-69.636925,570.3692,-65.76Q564.7938,-61.883076,561.2492,-56.64L561.2492,0Z" />
<path
android:fillColor="#53BDB6"
android:pathData="M647.59076,-34.486153Q647.88617,-25.772308,650.9877,-19.975384Q654.08923,-14.178461,659.1108,-11.409231Q664.1323,-8.64,670.3354,-8.64Q676.0954,-8.64,680.9323,-10.338462Q685.7692,-12.036923,691.0862,-15.655385L696.92,-7.4584618Q691.4554,-3.1753845,684.4031,-0.73846155Q677.35077,1.6984615,670.1877,1.6984615Q659.0369,1.6984615,650.9877,-3.323077Q642.9385,-8.344615,638.7662,-17.50154Q634.5939,-26.65846,634.5939,-38.76923Q634.5939,-50.51077,638.8031,-59.74154Q643.0123,-68.972305,650.6923,-74.17846Q658.3723,-79.38461,668.56305,-79.38461Q678.3108,-79.38461,685.43695,-74.80615Q692.56305,-70.22769,696.36615,-61.624615Q700.16925,-53.021538,700.16925,-41.28Q700.16925,-37.883076,699.87384,-34.486153ZM668.71075,-69.19385Q659.70154,-69.19385,654.0523,-62.88Q648.4031,-56.566154,647.6646,-44.086155L688.2062,-44.086155Q687.9846,-56.344616,682.81537,-62.76923Q677.6462,-69.19385,668.71075,-69.19385Z" />
<path
android:fillColor="#53BDB6"
android:pathData="M757.12,-19.2Q757.12,-13.735385,760.55383,-11.187693Q763.9877,-8.64,769.96924,-8.64Q776.24615,-8.64,783.04,-11.372308L786.3631,-2.2892308Q782.8185,-0.51692307,778.0923,0.59076923Q773.36615,1.6984615,767.90155,1.6984615Q761.0339,1.6984615,755.75385,-0.8861538Q750.4738,-3.4707692,747.5939,-8.344615Q744.71387,-13.218462,744.71387,-19.864614L744.71387,-99.24923L720.8615,-99.24923L720.8615,-109.07077L757.12,-109.07077Z" />
</group>
</group>
</group>
</group>
</vector>

View File

@ -3,14 +3,15 @@
android:height="108dp"
android:viewportWidth="640"
android:viewportHeight="640">
<group android:scaleX="0.6"
android:scaleY="0.6"
android:translateX="128"
android:translateY="128">
<path
android:pathData="M316.72,80.15C314.94,80.82 312.08,83.95 308.79,88.84C275.66,138.15 157.88,161.96 119.66,127.08C109.97,118.24 101.21,118.84 98.97,128.5C96.01,141.29 98.49,204.07 103.12,233.5C123.71,364.32 186.77,465.69 303.03,554.88C314.06,563.34 316.63,563.42 326.93,555.61C329.91,553.35 336.21,548.63 340.93,545.13C345.64,541.63 350.87,537.46 352.55,535.88C354.23,534.29 357.92,531.53 360.75,529.74C413.56,496.43 481.74,399.04 510.38,316C527.22,267.19 534.86,236.66 539.96,197.9C547.99,136.74 545.31,124.46 526,134.01C469.85,161.74 361.02,137.16 333.39,90.49C327.63,80.75 322.93,77.84 316.72,80.15M307.5,195.34C282.24,203.76 266.16,237.38 269.85,274.04C270.99,285.39 271.18,285 264.63,285C254.13,285 254.15,284.87 255,352.05C255.64,402.94 250.1,397.02 298.5,398.52C352.54,400.2 377.63,400.23 379.23,398.63C381.67,396.18 381.86,392.86 382.46,339.89C383.09,283.93 383.39,286 374.51,286C367.07,286 367.21,286.26 367.77,273.58C369.89,225.38 338.74,184.92 307.5,195.34M308.93,216.98C294.31,224.7 281.68,270.05 290.33,283.75C291.74,285.99 346.85,285.51 347.78,283.25C359.48,254.75 330.62,205.51 308.93,216.98M309.54,317.1C304.18,321.81 304.39,327.76 310.11,332.99C314.78,337.26 314.82,336.51 309.33,349.89C307.44,354.51 306.13,358.89 306.42,359.64C306.96,361.06 329,361.75 329,360.35C329,359.98 327.42,354.82 325.5,348.86C323.58,342.91 322,337.52 322,336.89C322,336.26 323.58,334 325.5,331.87C335.69,320.59 321.02,307.02 309.54,317.1"
android:fillColor="#53bdb6"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
</group>
<group
android:scaleX="0.6"
android:scaleY="0.6"
android:translateX="128"
android:translateY="128">
<path
android:fillColor="#53bdb6"
android:fillType="evenOdd"
android:pathData="M316.72,80.15C314.94,80.82 312.08,83.95 308.79,88.84C275.66,138.15 157.88,161.96 119.66,127.08C109.97,118.24 101.21,118.84 98.97,128.5C96.01,141.29 98.49,204.07 103.12,233.5C123.71,364.32 186.77,465.69 303.03,554.88C314.06,563.34 316.63,563.42 326.93,555.61C329.91,553.35 336.21,548.63 340.93,545.13C345.64,541.63 350.87,537.46 352.55,535.88C354.23,534.29 357.92,531.53 360.75,529.74C413.56,496.43 481.74,399.04 510.38,316C527.22,267.19 534.86,236.66 539.96,197.9C547.99,136.74 545.31,124.46 526,134.01C469.85,161.74 361.02,137.16 333.39,90.49C327.63,80.75 322.93,77.84 316.72,80.15M307.5,195.34C282.24,203.76 266.16,237.38 269.85,274.04C270.99,285.39 271.18,285 264.63,285C254.13,285 254.15,284.87 255,352.05C255.64,402.94 250.1,397.02 298.5,398.52C352.54,400.2 377.63,400.23 379.23,398.63C381.67,396.18 381.86,392.86 382.46,339.89C383.09,283.93 383.39,286 374.51,286C367.07,286 367.21,286.26 367.77,273.58C369.89,225.38 338.74,184.92 307.5,195.34M308.93,216.98C294.31,224.7 281.68,270.05 290.33,283.75C291.74,285.99 346.85,285.51 347.78,283.25C359.48,254.75 330.62,205.51 308.93,216.98M309.54,317.1C304.18,321.81 304.39,327.76 310.11,332.99C314.78,337.26 314.82,336.51 309.33,349.89C307.44,354.51 306.13,358.89 306.42,359.64C306.96,361.06 329,361.75 329,360.35C329,359.98 327.42,354.82 325.5,348.86C323.58,342.91 322,337.52 322,336.89C322,336.26 323.58,334 325.5,331.87C335.69,320.59 321.02,307.02 309.54,317.1"
android:strokeColor="#00000000" />
</group>
</vector>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_banner_background"/>
<foreground android:drawable="@drawable/ic_banner_foreground"/>
<background android:drawable="@color/ic_banner_background" />
<foreground android:drawable="@drawable/ic_banner_foreground" />
</adaptive-icon>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<!-- support for adaptive theme icons -->
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<!-- support for adaptive theme icons -->
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -1,6 +1,5 @@
<resources>
<string name="app_name">WG Tunnel</string>
<string name="tunnel_extras_key">tunnelConfig</string>
<string name="vpn_channel_id">VPN Channel</string>
<string name="vpn_channel_name">VPN Notification Channel</string>
<string name="watcher_channel_id">Watcher Channel</string>
@ -17,8 +16,8 @@
<string name="watcher_notification_title">Watcher Service</string>
<string name="watcher_notification_text_active">Monitoring network state changes: active</string>
<string name="watcher_notification_text_paused">Monitoring network state changes: paused</string>
<string name="tunnel_start_title">VPN Connected</string>
<string name="tunnel_start_text">Connected to tunnel -</string>
<string name="tunnel_start_title">VPN connected</string>
<string name="tunnel_start_text">Connected to tunnel</string>
<string name="vpn_permission_required">VPN permission is required for the app to work properly. If this permission is not launching, please disable \"Always-on VPN\" in your phone settings for the official WireGuard mobile app and try again.</string>
<string name="notification_permission_required">Notifications permission required.</string>
<string name="open_settings">Open Settings</string>
@ -103,7 +102,7 @@
<string name="default_vpn_on">Primary VPN on</string>
<string name="default_vpn_off">Primary VPN off</string>
<string name="create_import">Create from scratch</string>
<string name="turn_off_auto">Action requires auto-tunnel disabled</string>
<string name="turn_off_auto">Action requires auto-tunnel disabled or paused</string>
<string name="turn_on_tunnel">Action requires active tunnel</string>
<string name="add_peer">Add peer</string>
<string name="done">Done</string>
@ -121,8 +120,6 @@
<string name="seconds">seconds</string>
<string name="persistent_keepalive">Persistent keepalive</string>
<string name="cancel">Cancel</string>
<string name="primary_tunnel_change">Primary tunnel change</string>
<string name="primary_tunnel_change_question">Would you like to make this your primary tunnel?</string>
<string name="error_authentication_failed">Authentication failed</string>
<string name="error_authorization_failed">Failed to authorize</string>
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
@ -133,7 +130,6 @@
<string name="precise_location_required">Precise location required</string>
<string name="unknown_error">Unknown error occurred</string>
<string name="exported_configs_message">Exported configs to downloads</string>
<string name="status">status</string>
<string name="tunnel_on_wifi">Tunnel on untrusted wifi</string>
<string name="my_email" translatable="false">support@zaneschepke.com</string>
<string name="email_subject">WG Tunnel Support</string>
@ -179,5 +175,17 @@
<string name="enter_pin">Enter your pin</string>
<string name="create_pin">Create pin</string>
<string name="enable_app_lock">Enabled app lock</string>
<string name="restart_on_ping">Restart on ping fail</string>
<string name="restart_on_ping">Restart on ping fail (beta)</string>
<string name="mobile_data_tunnel">Set as mobile data tunnel</string>
<string name="set_primary_tunnel">Set as primary tunnel</string>
<string name="use_tunnel_on_wifi_name">Use tunnel on wifi name</string>
<string name="no_wifi_names_configured">No wifi names configured for this tunnel</string>
<string name="general">General</string>
<string name="edit_tunnel">Edit tunnel</string>
<string name="tunnel">Tunnel</string>
<string name="disabled">disabled</string>
<string name="auto_on">Resume auto tun</string>
<string name="auto_off">Pause auto tun</string>
<string name="auto_tun_on">Resume auto-tunnel</string>
<string name="auto_tun_off">Pause auto-tunnel</string>
</resources>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.WireguardAutoTunnel" parent="@style/Theme.AppCompat.NoActionBar">
<item name="android:windowBackground">@color/black_background</item>
</style>

View File

@ -33,4 +33,36 @@
</intent>
<capability-binding android:key="actions.intent.STOP" />
</shortcut>
<shortcut
android:enabled="true"
android:icon="@drawable/auto_play"
android:shortcutId="autoOn1"
android:shortcutLongLabel="@string/auto_on"
android:shortcutShortLabel="@string/auto_tun_on">
<intent
android:action="START"
android:targetClass="com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsActivity"
android:targetPackage="com.zaneschepke.wireguardautotunnel">
<extra
android:name="className"
android:value="WireGuardConnectivityWatcherService" />
</intent>
<capability-binding android:key="actions.intent.STOP" />
</shortcut>
<shortcut
android:enabled="true"
android:icon="@drawable/auto_pause"
android:shortcutId="autoOff1"
android:shortcutLongLabel="@string/auto_off"
android:shortcutShortLabel="@string/auto_tun_off">
<intent
android:action="STOP"
android:targetClass="com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsActivity"
android:targetPackage="com.zaneschepke.wireguardautotunnel">
<extra
android:name="className"
android:value="WireGuardConnectivityWatcherService" />
</intent>
<capability-binding android:key="actions.intent.STOP" />
</shortcut>
</shortcuts>

View File

@ -26,9 +26,10 @@ object BuildHelper {
val properties = java.util.Properties()
val localProperties = File(file)
if (localProperties.isFile) {
java.io.InputStreamReader(java.io.FileInputStream(localProperties), Charsets.UTF_8).use { reader ->
properties.load(reader)
}
java.io.InputStreamReader(java.io.FileInputStream(localProperties), Charsets.UTF_8)
.use { reader ->
properties.load(reader)
}
} else return null
return properties.getProperty(key)
}

View File

@ -1,12 +1,12 @@
object Constants {
const val VERSION_NAME = "3.3.9"
const val VERSION_NAME = "3.4.0"
const val JVM_TARGET = "17"
const val VERSION_CODE = 33900
const val VERSION_CODE = 34000
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.10"
const val COMPOSE_COMPILER_EXTENSION_VERSION = "1.5.11"
const val STORE_PASS_VAR = "SIGNING_STORE_PASSWORD"

View File

@ -0,0 +1,6 @@
What's new:
- Auto tunnel to specific tunnels by wifi name
- Auto tunnel control from tile and shortcuts
- Auto start manual tunnel if it was on before reboot
- Various bug fixes and performance improvements

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 125 KiB

View File

@ -21,15 +21,15 @@ roomVersion = "2.6.1"
timber = "5.0.1"
tunnel = "1.0.20230706"
androidGradlePlugin = "8.3.1"
kotlin = "1.9.22"
ksp = "1.9.22-1.0.17"
composeBom = "2024.02.02"
compose = "1.6.3"
kotlin = "1.9.23"
ksp = "1.9.23-1.0.19"
composeBom = "2024.03.00"
compose = "1.6.4"
zxingAndroidEmbedded = "4.3.0"
zxingCore = "3.5.3"
#plugins
gradlePlugins-kotlinxSerialization = "1.8.21"
gradlePlugins-kotlinxSerialization = "1.9.23"
material = "1.11.0"

View File

@ -19,7 +19,7 @@ android {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
"proguard-rules.pro",
)
}
}

Some files were not shown because too many files have changed in this diff Show More