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.
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
}
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>>
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
) {
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
) {
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
) {
|
||||
|
|
|
@ -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() }
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
|
|
|
@ -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 "",
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -57,6 +57,7 @@ fun WireguardAutoTunnelTheme(
|
|||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
||||
}
|
||||
|
|
|
@ -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 "")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 144 KiB |
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 81 KiB |
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 132 KiB |
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 125 KiB |
|
@ -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"
|
||||
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ android {
|
|||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|