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)
|
## 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).
|
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:
|
And then build the app:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ ./gradlew assembleDebug
|
$ ./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
|
@Test
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun migrate4To5() {
|
fun migrate6To7() {
|
||||||
helper.createDatabase(dbName, 4).apply {
|
helper.createDatabase(dbName, 6).apply {
|
||||||
// Database has schema version 1. Insert some data using SQL queries.
|
// Database has schema version 1. Insert some data using SQL queries.
|
||||||
// You can't use DAO classes because they expect the latest schema.
|
// You can't use DAO classes because they expect the latest schema.
|
||||||
execSQL(Queries.createDefaultSettings())
|
execSQL(Queries.createDefaultSettings())
|
||||||
execSQL(
|
execSQL(
|
||||||
"INSERT INTO TunnelConfig (name, wg_quick)" + " VALUES ('hello', 'hello')",
|
Queries.createTunnelConfig(),
|
||||||
)
|
)
|
||||||
// Prepare for the next version.
|
// Prepare for the next version.
|
||||||
close()
|
close()
|
||||||
|
@ -37,7 +37,7 @@ class MigrationTest {
|
||||||
|
|
||||||
// Re-open the database with version 2 and provide
|
// Re-open the database with version 2 and provide
|
||||||
// MIGRATION_1_2 as the migration process.
|
// MIGRATION_1_2 as the migration process.
|
||||||
helper.runMigrationsAndValidate(dbName, 5, true)
|
helper.runMigrationsAndValidate(dbName, 7, true)
|
||||||
// MigrationTestHelper automatically verifies the schema changes,
|
// MigrationTestHelper automatically verifies the schema changes,
|
||||||
// but you need to validate that the data was migrated properly.
|
// but you need to validate that the data was migrated properly.
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
|
@ -101,7 +102,24 @@
|
||||||
android:name=".service.tile.TunnelControlTile"
|
android:name=".service.tile.TunnelControlTile"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:icon="@drawable/ic_launcher"
|
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">
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.service.quicksettings.ACTIVE_TILE"
|
android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||||
|
@ -144,6 +162,7 @@
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
|
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
|
||||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||||
|
|
|
@ -5,6 +5,7 @@ import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
||||||
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
@ -19,6 +20,7 @@ class WireGuardAutoTunnel : Application() {
|
||||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) else Timber.plant(ReleaseTree())
|
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) else Timber.plant(ReleaseTree())
|
||||||
PinManager.initialize(this)
|
PinManager.initialize(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
lateinit var instance: WireGuardAutoTunnel
|
lateinit var instance: WireGuardAutoTunnel
|
||||||
private set
|
private set
|
||||||
|
@ -27,11 +29,18 @@ class WireGuardAutoTunnel : Application() {
|
||||||
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestTileServiceStateUpdate(context : Context) {
|
fun requestTunnelTileServiceStateUpdate(context: Context) {
|
||||||
TileService.requestListeningState(
|
TileService.requestListeningState(
|
||||||
context,
|
context,
|
||||||
ComponentName(instance, TunnelControlTile::class.java),
|
ComponentName(instance, TunnelControlTile::class.java),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun requestAutoTunnelTileServiceUpdate(context: Context) {
|
||||||
|
TileService.requestListeningState(
|
||||||
|
context,
|
||||||
|
ComponentName(instance, AutoTunnelControlTile::class.java),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,14 +2,16 @@ package com.zaneschepke.wireguardautotunnel.data
|
||||||
|
|
||||||
import androidx.room.AutoMigration
|
import androidx.room.AutoMigration
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
|
import androidx.room.DeleteColumn
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
|
import androidx.room.migration.AutoMigrationSpec
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [Settings::class, TunnelConfig::class],
|
entities = [Settings::class, TunnelConfig::class],
|
||||||
version = 6,
|
version = 7,
|
||||||
autoMigrations =
|
autoMigrations =
|
||||||
[
|
[
|
||||||
AutoMigration(from = 1, to = 2),
|
AutoMigration(from = 1, to = 2),
|
||||||
|
@ -26,6 +28,11 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
from = 5,
|
from = 5,
|
||||||
to = 6,
|
to = 6,
|
||||||
),
|
),
|
||||||
|
AutoMigration(
|
||||||
|
from = 6,
|
||||||
|
to = 7,
|
||||||
|
spec = RemoveLegacySettingColumnsMigration::class,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
)
|
)
|
||||||
|
@ -35,3 +42,13 @@ abstract class AppDatabase : RoomDatabase() {
|
||||||
|
|
||||||
abstract fun tunnelConfigDoa(): TunnelConfigDao
|
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 DatabaseListConverters {
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun stringToList(value: String): MutableList<String> {
|
fun stringToList(value: String): MutableList<String> {
|
||||||
if (value.isEmpty()) return mutableListOf()
|
if (value.isBlank() || value.isEmpty()) return mutableListOf()
|
||||||
return try {
|
return try {
|
||||||
Json.decodeFromString<MutableList<String>>(value)
|
Json.decodeFromString<MutableList<String>>(value)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
|
@ -6,11 +6,9 @@ object Queries {
|
||||||
INSERT INTO Settings (is_tunnel_enabled,
|
INSERT INTO Settings (is_tunnel_enabled,
|
||||||
is_tunnel_on_mobile_data_enabled,
|
is_tunnel_on_mobile_data_enabled,
|
||||||
trusted_network_ssids,
|
trusted_network_ssids,
|
||||||
default_tunnel,
|
|
||||||
is_always_on_vpn_enabled,
|
is_always_on_vpn_enabled,
|
||||||
is_tunnel_on_ethernet_enabled,
|
is_tunnel_on_ethernet_enabled,
|
||||||
is_shortcuts_enabled,
|
is_shortcuts_enabled,
|
||||||
is_battery_saver_enabled,
|
|
||||||
is_tunnel_on_wifi_enabled,
|
is_tunnel_on_wifi_enabled,
|
||||||
is_kernel_enabled,
|
is_kernel_enabled,
|
||||||
is_restore_on_boot_enabled,
|
is_restore_on_boot_enabled,
|
||||||
|
@ -19,8 +17,6 @@ object Queries {
|
||||||
('false',
|
('false',
|
||||||
'false',
|
'false',
|
||||||
'sampleSSID1,sampleSSID2',
|
'sampleSSID1,sampleSSID2',
|
||||||
NULL,
|
|
||||||
'false',
|
|
||||||
'false',
|
'false',
|
||||||
'false',
|
'false',
|
||||||
'false',
|
'false',
|
||||||
|
@ -30,4 +26,10 @@ object Queries {
|
||||||
'false')
|
'false')
|
||||||
""".trimIndent()
|
""".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
|
@Dao
|
||||||
interface SettingsDao {
|
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.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface TunnelConfigDao {
|
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,16 +4,24 @@ import android.content.Context
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.core.intPreferencesKey
|
||||||
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
class DataStoreManager(private val context: Context) {
|
class DataStoreManager(private val context: Context) {
|
||||||
companion object {
|
companion object {
|
||||||
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
|
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
|
||||||
val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
|
val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
|
||||||
|
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
|
// preferences
|
||||||
|
@ -24,16 +32,35 @@ class DataStoreManager(private val context: Context) {
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun init() {
|
suspend fun init() {
|
||||||
|
try {
|
||||||
context.dataStore.data.first()
|
context.dataStore.data.first()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.e(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) =
|
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) {
|
||||||
|
try {
|
||||||
context.dataStore.edit { it[key] = value }
|
context.dataStore.edit { it[key] = value }
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.e(e)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] }
|
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] }
|
||||||
|
|
||||||
suspend fun <T> getFromStore(key: Preferences.Key<T>) =
|
suspend fun <T> getFromStore(key: Preferences.Key<T>): T? {
|
||||||
|
return try {
|
||||||
context.dataStore.data.map { it[key] }.first()
|
context.dataStore.data.map { it[key] }.first()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.e(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun <T> getFromStoreBlocking(key: Preferences.Key<T>) = runBlocking {
|
fun <T> getFromStoreBlocking(key: Preferences.Key<T>) = runBlocking {
|
||||||
context.dataStore.data.map { it[key] }.first()
|
context.dataStore.data.map { it[key] }.first()
|
||||||
|
|
|
@ -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
|
@Entity
|
||||||
data class Settings(
|
data class Settings(
|
||||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
@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")
|
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled")
|
||||||
var isTunnelOnMobileDataEnabled: Boolean = false,
|
val isTunnelOnMobileDataEnabled: Boolean = false,
|
||||||
@ColumnInfo(name = "trusted_network_ssids")
|
@ColumnInfo(name = "trusted_network_ssids")
|
||||||
var trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
|
val trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
|
||||||
@ColumnInfo(name = "default_tunnel") var defaultTunnel: String? = null,
|
@ColumnInfo(name = "is_always_on_vpn_enabled") val isAlwaysOnVpnEnabled: Boolean = false,
|
||||||
@ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled: Boolean = false,
|
|
||||||
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled")
|
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled")
|
||||||
var isTunnelOnEthernetEnabled: Boolean = false,
|
val isTunnelOnEthernetEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_shortcuts_enabled",
|
name = "is_shortcuts_enabled",
|
||||||
defaultValue = "false",
|
defaultValue = "false",
|
||||||
)
|
)
|
||||||
var isShortcutsEnabled: Boolean = false,
|
val isShortcutsEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
|
||||||
name = "is_battery_saver_enabled",
|
|
||||||
defaultValue = "false",
|
|
||||||
)
|
|
||||||
var isBatterySaverEnabled: Boolean = false,
|
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_tunnel_on_wifi_enabled",
|
name = "is_tunnel_on_wifi_enabled",
|
||||||
defaultValue = "false",
|
defaultValue = "false",
|
||||||
)
|
)
|
||||||
var isTunnelOnWifiEnabled: Boolean = false,
|
val isTunnelOnWifiEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_kernel_enabled",
|
name = "is_kernel_enabled",
|
||||||
defaultValue = "false",
|
defaultValue = "false",
|
||||||
)
|
)
|
||||||
var isKernelEnabled: Boolean = false,
|
val isKernelEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_restore_on_boot_enabled",
|
name = "is_restore_on_boot_enabled",
|
||||||
defaultValue = "false",
|
defaultValue = "false",
|
||||||
)
|
)
|
||||||
var isRestoreOnBootEnabled: Boolean = false,
|
val isRestoreOnBootEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_multi_tunnel_enabled",
|
name = "is_multi_tunnel_enabled",
|
||||||
defaultValue = "false",
|
defaultValue = "false",
|
||||||
)
|
)
|
||||||
var isMultiTunnelEnabled: Boolean = false,
|
val isMultiTunnelEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_auto_tunnel_paused",
|
name = "is_auto_tunnel_paused",
|
||||||
defaultValue = "false",
|
defaultValue = "false",
|
||||||
)
|
)
|
||||||
var isAutoTunnelPaused: Boolean = false,
|
val isAutoTunnelPaused: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_ping_enabled",
|
name = "is_ping_enabled",
|
||||||
defaultValue = "false",
|
defaultValue = "false",
|
||||||
)
|
)
|
||||||
var isPingEnabled: Boolean = false,
|
val isPingEnabled: Boolean = false,
|
||||||
) {
|
)
|
||||||
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean {
|
|
||||||
return if (defaultTunnel != null) {
|
|
||||||
val defaultConfig = TunnelConfig.from(defaultTunnel!!)
|
|
||||||
(tunnelConfig.id == defaultConfig.id)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,26 +5,30 @@ import androidx.room.Entity
|
||||||
import androidx.room.Index
|
import androidx.room.Index
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
@Entity(indices = [Index(value = ["name"], unique = true)])
|
@Entity(indices = [Index(value = ["name"], unique = true)])
|
||||||
@Serializable
|
|
||||||
data class TunnelConfig(
|
data class TunnelConfig(
|
||||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||||
@ColumnInfo(name = "name") var name: String,
|
@ColumnInfo(name = "name") val name: String,
|
||||||
@ColumnInfo(name = "wg_quick") var wgQuick: 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 {
|
companion object {
|
||||||
fun from(string: String): TunnelConfig {
|
|
||||||
return Json.decodeFromString<TunnelConfig>(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun configFromQuick(wgQuick: String): Config {
|
fun configFromQuick(wgQuick: String): Config {
|
||||||
val inputStream: InputStream = wgQuick.byteInputStream()
|
val inputStream: InputStream = wgQuick.byteInputStream()
|
||||||
val reader = inputStream.bufferedReader(Charsets.UTF_8)
|
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 com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import kotlinx.coroutines.flow.Flow
|
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) {
|
override suspend fun save(settings: Settings) {
|
||||||
settingsDoa.save(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 save(tunnelConfig: TunnelConfig)
|
||||||
|
|
||||||
|
suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?)
|
||||||
|
|
||||||
|
suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?)
|
||||||
|
|
||||||
suspend fun delete(tunnelConfig: TunnelConfig)
|
suspend fun delete(tunnelConfig: TunnelConfig)
|
||||||
|
|
||||||
|
suspend fun getById(id: Int): TunnelConfig?
|
||||||
|
|
||||||
suspend fun count(): Int
|
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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,4 +2,6 @@ package com.zaneschepke.wireguardautotunnel.module
|
||||||
|
|
||||||
import javax.inject.Qualifier
|
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.SettingsDao
|
||||||
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
||||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||||
|
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.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepositoryImpl
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepositoryImpl
|
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
@ -34,13 +38,13 @@ class RepositoryModule {
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao): TunnelConfigRepository {
|
fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao): TunnelConfigRepository {
|
||||||
return TunnelConfigRepositoryImpl(tunnelConfigDao)
|
return RoomTunnelConfigRepository(tunnelConfigDao)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun provideSettingsRepository(settingsDao: SettingsDao): SettingsRepository {
|
fun provideSettingsRepository(settingsDao: SettingsDao): SettingsRepository {
|
||||||
return SettingsRepositoryImpl(settingsDao)
|
return RoomSettingsRepository(settingsDao)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
|
@ -48,4 +52,22 @@ class RepositoryModule {
|
||||||
fun providePreferencesDataStore(@ApplicationContext context: Context): DataStoreManager {
|
fun providePreferencesDataStore(@ApplicationContext context: Context): DataStoreManager {
|
||||||
return DataStoreManager(context)
|
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.backend.WgQuickBackend
|
||||||
import com.wireguard.android.util.RootShell
|
import com.wireguard.android.util.RootShell
|
||||||
import com.wireguard.android.util.ToolsInstaller
|
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.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
|
@ -44,8 +45,14 @@ class TunnelModule {
|
||||||
fun provideVpnService(
|
fun provideVpnService(
|
||||||
@Userspace userspaceBackend: Backend,
|
@Userspace userspaceBackend: Backend,
|
||||||
@Kernel kernelBackend: Backend,
|
@Kernel kernelBackend: Backend,
|
||||||
settingsRepository: SettingsRepository
|
appDataRepository: AppDataRepository
|
||||||
): VpnService {
|
): 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
|
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.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.util.goAsync
|
import com.zaneschepke.wireguardautotunnel.util.goAsync
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
@ -14,19 +13,33 @@ import javax.inject.Inject
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class BootReceiver : BroadcastReceiver() {
|
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 {
|
override fun onReceive(context: Context?, intent: Intent?) = goAsync {
|
||||||
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
|
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
|
||||||
val settings = settingsRepository.getSettings()
|
context?.run {
|
||||||
|
val settings = appDataRepository.settings.getSettings()
|
||||||
if (settings.isAutoTunnelEnabled) {
|
if (settings.isAutoTunnelEnabled) {
|
||||||
Timber.i("Starting watcher service from boot")
|
Timber.i("Starting watcher service from boot")
|
||||||
ServiceManager.startWatcherServiceForeground(context!!)
|
serviceManager.startWatcherServiceForeground(context)
|
||||||
} else if(settings.isAlwaysOnVpnEnabled) {
|
}
|
||||||
Timber.i("Starting tunnel from boot")
|
if (appDataRepository.appState.isTunnelRunningFromManualStart()) {
|
||||||
ServiceManager.startVpnServicePrimaryTunnel(context!!, settings, tunnelConfigRepository.getAll().firstOrNull())
|
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 dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class NotificationActionReceiver : BroadcastReceiver() {
|
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 {
|
override fun onReceive(context: Context, intent: Intent?) = goAsync {
|
||||||
try {
|
try {
|
||||||
val settings = settingsRepository.getSettings()
|
//TODO fix for manual start changes when enabled
|
||||||
if (settings.defaultTunnel != null) {
|
serviceManager.stopVpnService(context)
|
||||||
ServiceManager.stopVpnService(context)
|
|
||||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||||
ServiceManager.startVpnServiceForeground(context, settings.defaultTunnel.toString())
|
serviceManager.startVpnServiceForeground(context)
|
||||||
}
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
} finally {
|
} finally {
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.lifecycle.LifecycleService
|
import androidx.lifecycle.LifecycleService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
open class ForegroundService : LifecycleService() {
|
open class ForegroundService : LifecycleService() {
|
||||||
|
@ -23,11 +24,13 @@ open class ForegroundService : LifecycleService() {
|
||||||
when (action) {
|
when (action) {
|
||||||
Action.START.name,
|
Action.START.name,
|
||||||
Action.START_FOREGROUND.name -> startService(intent.extras)
|
Action.START_FOREGROUND.name -> startService(intent.extras)
|
||||||
|
|
||||||
Action.STOP.name -> stopService(intent.extras)
|
Action.STOP.name -> stopService(intent.extras)
|
||||||
"android.net.VpnService" -> {
|
Constants.ALWAYS_ON_VPN_ACTION -> {
|
||||||
Timber.d("Always-on VPN starting service")
|
Timber.i("Always-on VPN starting service")
|
||||||
startService(intent.extras)
|
startService(intent.extras)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> Timber.d("This should never happen. No action in the received intent")
|
else -> Timber.d("This should never happen. No action in the received intent")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -3,33 +3,17 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
object ServiceManager {
|
class ServiceManager(private val appDataRepository: AppDataRepository) {
|
||||||
|
|
||||||
// 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
|
|
||||||
// }
|
|
||||||
|
|
||||||
private fun <T : Service> actionOnService(
|
private fun <T : Service> actionOnService(
|
||||||
action: Action,
|
action: Action,
|
||||||
context: Context,
|
context: Context,
|
||||||
cls: Class<T>,
|
cls: Class<T>,
|
||||||
extras: Map<String, String>? = null
|
extras: Map<String, Int>? = null
|
||||||
) {
|
) {
|
||||||
val intent =
|
val intent =
|
||||||
Intent(context, cls).also {
|
Intent(context, cls).also {
|
||||||
|
@ -42,9 +26,11 @@ object ServiceManager {
|
||||||
Action.START_FOREGROUND -> {
|
Action.START_FOREGROUND -> {
|
||||||
context.startForegroundService(intent)
|
context.startForegroundService(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
Action.START -> {
|
Action.START -> {
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
Action.STOP -> context.startService(intent)
|
Action.STOP -> context.startService(intent)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} 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(
|
actionOnService(
|
||||||
Action.START,
|
Action.START,
|
||||||
context,
|
context,
|
||||||
WireGuardTunnelService::class.java,
|
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")
|
Timber.d("Stopping vpn service action")
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.STOP,
|
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(
|
actionOnService(
|
||||||
Action.START_FOREGROUND,
|
Action.START_FOREGROUND,
|
||||||
context,
|
context,
|
||||||
WireGuardTunnelService::class.java,
|
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(
|
fun startWatcherServiceForeground(
|
||||||
context: Context,
|
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 androidx.lifecycle.lifecycleScope
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
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.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
||||||
|
@ -33,29 +33,29 @@ import javax.inject.Inject
|
||||||
class WireGuardConnectivityWatcherService : ForegroundService() {
|
class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
private val foregroundId = 122
|
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())
|
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 lateinit var watcherJob: Job
|
||||||
|
|
||||||
private var wakeLock: PowerManager.WakeLock? = null
|
private var wakeLock: PowerManager.WakeLock? = null
|
||||||
|
@ -65,7 +65,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
if (settingsRepository.getSettings().isAutoTunnelPaused) {
|
if (appDataRepository.settings.getSettings().isAutoTunnelPaused) {
|
||||||
launchWatcherPausedNotification()
|
launchWatcherPausedNotification()
|
||||||
} else launchWatcherNotification()
|
} else launchWatcherNotification()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -142,7 +142,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
private fun startWatcherJob() {
|
private fun startWatcherJob() {
|
||||||
watcherJob =
|
watcherJob =
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val setting = settingsRepository.getSettings()
|
val setting = appDataRepository.settings.getSettings()
|
||||||
launch {
|
launch {
|
||||||
Timber.i("Starting wifi watcher")
|
Timber.i("Starting wifi watcher")
|
||||||
watchForWifiConnectivityChanges()
|
watchForWifiConnectivityChanges()
|
||||||
|
@ -191,6 +191,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
isMobileDataConnected = true,
|
isMobileDataConnected = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
|
@ -198,6 +199,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
)
|
)
|
||||||
Timber.i("Mobile data capabilities changed")
|
Timber.i("Mobile data capabilities changed")
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkStatus.Unavailable -> {
|
is NetworkStatus.Unavailable -> {
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
|
@ -213,38 +215,38 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
try {
|
try {
|
||||||
do {
|
do {
|
||||||
if (vpnService.vpnState.value.status == Tunnel.State.UP) {
|
if (vpnService.vpnState.value.status == Tunnel.State.UP) {
|
||||||
val config = vpnService.vpnState.value.config
|
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
|
||||||
config?.let {
|
tunnelConfig?.let {
|
||||||
val results = it.peers.map { peer ->
|
val config = TunnelConfig.configFromQuick(it.wgQuick)
|
||||||
|
val results = config.peers.map { peer ->
|
||||||
val host = if (peer.endpoint.isPresent &&
|
val host = if (peer.endpoint.isPresent &&
|
||||||
peer.endpoint.get().resolved.isPresent)
|
peer.endpoint.get().resolved.isPresent)
|
||||||
peer.endpoint.get().resolved.get().host
|
peer.endpoint.get().resolved.get().host
|
||||||
else Constants.BACKUP_PING_HOST
|
else Constants.BACKUP_PING_HOST
|
||||||
Timber.i("Checking reachability of: $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")
|
Timber.i("Result: reachable - $reachable")
|
||||||
reachable
|
reachable
|
||||||
}
|
}
|
||||||
if (results.contains(false)) {
|
if (results.contains(false)) {
|
||||||
Timber.i("Restarting VPN for ping failure")
|
Timber.i("Restarting VPN for ping failure")
|
||||||
ServiceManager.stopVpnService(this)
|
serviceManager.stopVpnService(this)
|
||||||
delay(Constants.VPN_RESTART_DELAY)
|
delay(Constants.VPN_RESTART_DELAY)
|
||||||
val tunnel = networkEventsFlow.value.settings.defaultTunnel
|
serviceManager.startVpnServiceForeground(this)
|
||||||
ServiceManager.startVpnServiceForeground(this, tunnel!!)
|
|
||||||
delay(Constants.PING_COOLDOWN)
|
delay(Constants.PING_COOLDOWN)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delay(Constants.PING_INTERVAL)
|
delay(Constants.PING_INTERVAL)
|
||||||
} while (true)
|
} while (true)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun watchForSettingsChanges() {
|
private suspend fun watchForSettingsChanges() {
|
||||||
settingsRepository.getSettingsFlow().collect {
|
appDataRepository.settings.getSettingsFlow().collect {
|
||||||
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
|
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
|
||||||
when (it.isAutoTunnelPaused) {
|
when (it.isAutoTunnelPaused) {
|
||||||
true -> launchWatcherPausedNotification()
|
true -> launchWatcherPausedNotification()
|
||||||
|
@ -260,19 +262,11 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
|
|
||||||
private suspend fun watchForVpnConnectivityChanges() {
|
private suspend fun watchForVpnConnectivityChanges() {
|
||||||
vpnService.vpnState.collect {
|
vpnService.vpnState.collect {
|
||||||
when (it.status) {
|
|
||||||
Tunnel.State.DOWN ->
|
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
isVpnConnected = false,
|
vpnStatus = it.status,
|
||||||
|
config = it.tunnelConfig,
|
||||||
)
|
)
|
||||||
Tunnel.State.UP ->
|
|
||||||
networkEventsFlow.value =
|
|
||||||
networkEventsFlow.value.copy(
|
|
||||||
isVpnConnected = true,
|
|
||||||
)
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -286,6 +280,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
isEthernetConnected = true,
|
isEthernetConnected = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
Timber.i("Ethernet capabilities changed")
|
Timber.i("Ethernet capabilities changed")
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
|
@ -293,6 +288,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
isEthernetConnected = true,
|
isEthernetConnected = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkStatus.Unavailable -> {
|
is NetworkStatus.Unavailable -> {
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
|
@ -314,19 +310,24 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
isWifiConnected = true,
|
isWifiConnected = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
Timber.i("Wifi capabilities changed")
|
Timber.i("Wifi capabilities changed")
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
isWifiConnected = true,
|
isWifiConnected = true,
|
||||||
)
|
)
|
||||||
val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: ""
|
val ssid = wifiService.getNetworkName(it.networkCapabilities)
|
||||||
|
ssid?.let {
|
||||||
Timber.i("Detected SSID: $ssid")
|
Timber.i("Detected SSID: $ssid")
|
||||||
|
appDataRepository.appState.setCurrentSsid(ssid)
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
currentNetworkSSID = ssid,
|
currentNetworkSSID = ssid,
|
||||||
)
|
)
|
||||||
|
} ?: Timber.w("Failed to read ssid")
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkStatus.Unavailable -> {
|
is NetworkStatus.Unavailable -> {
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
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() {
|
private suspend fun manageVpn() {
|
||||||
networkEventsFlow.collectLatest {
|
networkEventsFlow.collectLatest { watcherState ->
|
||||||
val autoTunnel = "Auto-tunnel watcher"
|
val autoTunnel = "Auto-tunnel watcher"
|
||||||
if (!it.settings.isAutoTunnelPaused && it.settings.defaultTunnel != null) {
|
if (!watcherState.settings.isAutoTunnelPaused) {
|
||||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
//delay for rapid network state changes and then collect latest
|
||||||
|
delay(Constants.WATCHER_COLLECTION_DELAY)
|
||||||
when {
|
when {
|
||||||
((it.isEthernetConnected &&
|
watcherState.isEthernetConditionMet() -> {
|
||||||
it.settings.isTunnelOnEthernetEnabled &&
|
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
|
||||||
!it.isVpnConnected)) -> {
|
serviceManager.startVpnServiceForeground(this)
|
||||||
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
|
||||||
Timber.i("$autoTunnel condition 1 met")
|
|
||||||
}
|
}
|
||||||
(!it.isEthernetConnected &&
|
|
||||||
it.settings.isTunnelOnMobileDataEnabled &&
|
watcherState.isMobileDataConditionMet() -> {
|
||||||
!it.isWifiConnected &&
|
Timber.i("$autoTunnel - tunnel on on mobile data condition met")
|
||||||
it.isMobileDataConnected &&
|
serviceManager.startVpnServiceForeground(this, getMobileDataTunnel()?.id)
|
||||||
!it.isVpnConnected) -> {
|
|
||||||
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
|
||||||
Timber.i("$autoTunnel condition 2 met")
|
|
||||||
}
|
}
|
||||||
(!it.isEthernetConnected &&
|
|
||||||
!it.settings.isTunnelOnMobileDataEnabled &&
|
watcherState.isTunnelNotMobileDataPreferredConditionMet() -> {
|
||||||
!it.isWifiConnected &&
|
getMobileDataTunnel()?.let {
|
||||||
it.isVpnConnected) -> {
|
Timber.i("$autoTunnel - tunnel connected on mobile data is not preferred condition met, switching to preferred")
|
||||||
ServiceManager.stopVpnService(this)
|
serviceManager.startVpnServiceForeground(
|
||||||
Timber.i("$autoTunnel condition 3 met")
|
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")
|
|
||||||
}
|
}
|
||||||
(!it.isEthernetConnected &&
|
|
||||||
(it.isWifiConnected &&
|
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
|
||||||
it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) &&
|
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
|
||||||
(it.isVpnConnected)) -> {
|
serviceManager.stopVpnService(this)
|
||||||
ServiceManager.stopVpnService(this)
|
|
||||||
Timber.i("$autoTunnel condition 5 met")
|
|
||||||
}
|
}
|
||||||
(!it.isEthernetConnected &&
|
|
||||||
(it.isWifiConnected &&
|
watcherState.isTunnelNotWifiNamePreferredMet(watcherState.currentNetworkSSID) -> {
|
||||||
!it.settings.isTunnelOnWifiEnabled &&
|
Timber.i("$autoTunnel - tunnel on ssid not associated with current tunnel condition met")
|
||||||
(it.isVpnConnected))) -> {
|
getSsidTunnel(watcherState.currentNetworkSSID)?.let {
|
||||||
ServiceManager.stopVpnService(this)
|
Timber.i("Found tunnel associated with this SSID, bringing tunnel up")
|
||||||
Timber.i("$autoTunnel condition 6 met")
|
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)
|
||||||
}
|
}
|
||||||
(!it.isEthernetConnected &&
|
}.invoke()
|
||||||
!it.isWifiConnected &&
|
|
||||||
!it.isMobileDataConnected &&
|
|
||||||
(it.isVpnConnected)) -> {
|
|
||||||
ServiceManager.stopVpnService(this)
|
|
||||||
Timber.i("$autoTunnel condition 7 met")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watcherState.isUntrustedWifiConditionMet() -> {
|
||||||
|
Timber.i("$autoTunnel - tunnel on untrusted wifi condition met")
|
||||||
|
serviceManager.startVpnServiceForeground(
|
||||||
|
this,
|
||||||
|
getSsidTunnel(watcherState.currentNetworkSSID)?.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 -> {
|
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 android.os.Bundle
|
||||||
import androidx.core.app.ServiceCompat
|
import androidx.core.app.ServiceCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
|
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
|
||||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||||
|
@ -21,30 +20,30 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class WireGuardTunnelService : ForegroundService() {
|
class WireGuardTunnelService : ForegroundService() {
|
||||||
private val foregroundId = 123
|
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 lateinit var job: Job
|
||||||
|
|
||||||
private var tunnelName: String = ""
|
|
||||||
private var didShowConnected = false
|
private var didShowConnected = false
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
if (tunnelConfigRepository.getAll().isNotEmpty()) {
|
//TODO fix this to not launch if AOVPN
|
||||||
|
if (appDataRepository.tunnels.count() != 0) {
|
||||||
launchVpnNotification()
|
launchVpnNotification()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,44 +52,28 @@ class WireGuardTunnelService : ForegroundService() {
|
||||||
override fun startService(extras: Bundle?) {
|
override fun startService(extras: Bundle?) {
|
||||||
super.startService(extras)
|
super.startService(extras)
|
||||||
cancelJob()
|
cancelJob()
|
||||||
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
|
|
||||||
val tunnelConfig = tunnelConfigString?.let { TunnelConfig.from(it) }
|
|
||||||
tunnelName = tunnelConfig?.name ?: ""
|
|
||||||
job =
|
job =
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
launch {
|
launch {
|
||||||
if (tunnelConfig != null) {
|
val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY)
|
||||||
try {
|
if (vpnService.getState() == Tunnel.State.UP) {
|
||||||
tunnelName = tunnelConfig.name
|
vpnService.stopTunnel()
|
||||||
vpnService.startTunnel(tunnelConfig)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e("Problem starting tunnel: ${e.message}")
|
|
||||||
stopService(extras)
|
|
||||||
}
|
}
|
||||||
} else {
|
vpnService.startTunnel(
|
||||||
Timber.i("Tunnel config null, starting default tunnel or first")
|
tunnelId?.let {
|
||||||
val settings = settingsRepository.getSettings()
|
appDataRepository.tunnels.getById(it)
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO add failed to connect notification
|
|
||||||
launch {
|
launch {
|
||||||
|
handshakeNotifications()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO improve tunnel notifications
|
||||||
|
private suspend fun handshakeNotifications() {
|
||||||
|
var tunnelName: String? = null
|
||||||
vpnService.vpnState.collect { state ->
|
vpnService.vpnState.collect { state ->
|
||||||
state.statistics
|
state.statistics
|
||||||
?.mapPeerStats()
|
?.mapPeerStats()
|
||||||
|
@ -100,27 +83,38 @@ class WireGuardTunnelService : ForegroundService() {
|
||||||
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
|
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
|
||||||
if (!didShowConnected) {
|
if (!didShowConnected) {
|
||||||
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
|
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
|
||||||
|
tunnelName = state.tunnelConfig?.name
|
||||||
launchVpnNotification(
|
launchVpnNotification(
|
||||||
getString(R.string.tunnel_start_title),
|
getString(R.string.tunnel_start_title),
|
||||||
"${getString(R.string.tunnel_start_text)} $tunnelName",
|
"${getString(R.string.tunnel_start_text)} - $tunnelName",
|
||||||
)
|
)
|
||||||
didShowConnected = true
|
didShowConnected = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
|
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
|
||||||
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
|
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
|
||||||
true -> {}
|
true -> {
|
||||||
|
}
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (state.status == 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() {
|
private fun launchAlwaysOnDisabledNotification() {
|
||||||
launchVpnNotification(title = this.getString(R.string.vpn_connection_failed),
|
launchVpnNotification(
|
||||||
description = this.getString(R.string.always_on_disabled))
|
title = this.getString(R.string.vpn_connection_failed),
|
||||||
|
description = this.getString(R.string.always_on_disabled),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stopService(extras: Bundle?) {
|
override fun stopService(extras: Bundle?) {
|
||||||
|
|
|
@ -53,6 +53,7 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
object : ConnectivityManager.NetworkCallback() {
|
object : ConnectivityManager.NetworkCallback() {
|
||||||
override fun onAvailable(network: Network) {
|
override fun onAvailable(network: Network) {
|
||||||
|
|
|
@ -1,80 +1,66 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.service.shortcut
|
package com.zaneschepke.wireguardautotunnel.service.shortcut
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
|
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ShortcutsActivity : ComponentActivity() {
|
class ShortcutsActivity : ComponentActivity() {
|
||||||
@Inject lateinit var settingsRepository: SettingsRepository
|
|
||||||
|
|
||||||
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
@Inject
|
||||||
|
lateinit var appDataRepository: AppDataRepository
|
||||||
|
|
||||||
private suspend fun toggleWatcherServicePause() {
|
@Inject
|
||||||
val settings = settingsRepository.getSettings()
|
lateinit var serviceManager: ServiceManager
|
||||||
if (settings.isAutoTunnelEnabled) {
|
|
||||||
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
|
||||||
settingsRepository.save(
|
|
||||||
settings.copy(
|
|
||||||
isAutoTunnelPaused = pauseAutoTunnel,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(View(this))
|
|
||||||
if (
|
|
||||||
intent
|
|
||||||
.getStringExtra(CLASS_NAME_EXTRA_KEY)
|
|
||||||
.equals(WireGuardTunnelService::class.java.simpleName)
|
|
||||||
) {
|
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
val settings = settingsRepository.getSettings()
|
val settings = appDataRepository.settings.getSettings()
|
||||||
if (settings.isShortcutsEnabled) {
|
if (settings.isShortcutsEnabled) {
|
||||||
try {
|
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
|
||||||
|
WireGuardTunnelService::class.java.simpleName -> {
|
||||||
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
|
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
|
||||||
val tunnelConfig =
|
val tunnelConfig = tunnelName?.let {
|
||||||
if (tunnelName != null) {
|
appDataRepository.tunnels.getAll().firstOrNull {
|
||||||
tunnelConfigRepository.getAll().firstOrNull {
|
|
||||||
it.name == tunnelName
|
it.name == tunnelName
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (settings.defaultTunnel == null) {
|
|
||||||
tunnelConfigRepository.getAll().first()
|
|
||||||
} else {
|
|
||||||
TunnelConfig.from(settings.defaultTunnel!!)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
tunnelConfig ?: return@launch
|
|
||||||
toggleWatcherServicePause()
|
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
Action.STOP.name ->
|
Action.START.name -> serviceManager.startVpnServiceForeground(
|
||||||
ServiceManager.stopVpnService(
|
this@ShortcutsActivity, tunnelConfig?.id, isManualStart = true,
|
||||||
this@ShortcutsActivity,
|
|
||||||
)
|
)
|
||||||
Action.START.name ->
|
|
||||||
ServiceManager.startVpnServiceForeground(
|
Action.STOP.name -> serviceManager.stopVpnService(
|
||||||
this@ShortcutsActivity,
|
this@ShortcutsActivity,
|
||||||
tunnelConfig.toString(),
|
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,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e.message)
|
|
||||||
finish()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 android.service.quicksettings.TileService
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
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.data.repository.TunnelConfigRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
@ -18,40 +17,44 @@ import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@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 val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
private var tunnelName: String? = null
|
private var manualStartConfig: TunnelConfig? = null
|
||||||
|
|
||||||
override fun onStartListening() {
|
override fun onStartListening() {
|
||||||
super.onStartListening()
|
super.onStartListening()
|
||||||
Timber.d("On start listening called")
|
Timber.d("On start listening called")
|
||||||
scope.launch {
|
scope.launch {
|
||||||
vpnService.vpnState.collect {
|
vpnService.vpnState.collect { it ->
|
||||||
when (it.status) {
|
when (it.status) {
|
||||||
Tunnel.State.UP -> setActive()
|
Tunnel.State.UP -> {
|
||||||
Tunnel.State.DOWN -> setInactive()
|
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()
|
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 {
|
unlockAndRun {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
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) {
|
if (vpnService.getState() == Tunnel.State.UP) {
|
||||||
ServiceManager.stopVpnService(this@TunnelControlTile)
|
serviceManager.stopVpnService(this@TunnelControlTile, isManualStop = true)
|
||||||
} else {
|
} else {
|
||||||
ServiceManager.startVpnServiceForeground(
|
serviceManager.startVpnServiceForeground(
|
||||||
this@TunnelControlTile,
|
this@TunnelControlTile, manualStartConfig?.id, isManualStart = true,
|
||||||
config.toString(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} 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() {
|
private fun setActive() {
|
||||||
qsTile.state = Tile.STATE_ACTIVE
|
qsTile.state = Tile.STATE_ACTIVE
|
||||||
qsTile.updateTile()
|
qsTile.updateTile()
|
||||||
|
@ -119,6 +101,7 @@ class TunnelControlTile() : TileService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setUnavailable() {
|
private fun setUnavailable() {
|
||||||
|
manualStartConfig = null
|
||||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||||
qsTile.updateTile()
|
qsTile.updateTile()
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
interface VpnService : Tunnel {
|
interface VpnService : Tunnel {
|
||||||
suspend fun startTunnel(tunnelConfig: TunnelConfig): Tunnel.State
|
suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): Tunnel.State
|
||||||
|
|
||||||
suspend fun stopTunnel()
|
suspend fun stopTunnel()
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,10 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||||
|
|
||||||
import com.wireguard.android.backend.Statistics
|
import com.wireguard.android.backend.Statistics
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.wireguard.config.Config
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
|
||||||
data class VpnState(
|
data class VpnState(
|
||||||
val status: Tunnel.State = Tunnel.State.DOWN,
|
val status: Tunnel.State = Tunnel.State.DOWN,
|
||||||
val name: String = "",
|
val tunnelConfig: TunnelConfig? = null,
|
||||||
val config: Config? = null,
|
|
||||||
val statistics: Statistics? = 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.BackendException
|
||||||
import com.wireguard.android.backend.Statistics
|
import com.wireguard.android.backend.Statistics
|
||||||
import com.wireguard.android.backend.Tunnel.State
|
import com.wireguard.android.backend.Tunnel.State
|
||||||
import com.wireguard.config.Config
|
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
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.Kernel
|
||||||
import com.zaneschepke.wireguardautotunnel.module.Userspace
|
import com.zaneschepke.wireguardautotunnel.module.Userspace
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
@ -27,7 +26,7 @@ class WireGuardTunnel
|
||||||
constructor(
|
constructor(
|
||||||
@Userspace private val userspaceBackend: Backend,
|
@Userspace private val userspaceBackend: Backend,
|
||||||
@Kernel private val kernelBackend: Backend,
|
@Kernel private val kernelBackend: Backend,
|
||||||
private val settingsRepository: SettingsRepository
|
private val appDataRepository: AppDataRepository,
|
||||||
) : VpnService {
|
) : VpnService {
|
||||||
private val _vpnState = MutableStateFlow(VpnState())
|
private val _vpnState = MutableStateFlow(VpnState())
|
||||||
override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow()
|
override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow()
|
||||||
|
@ -36,15 +35,13 @@ constructor(
|
||||||
|
|
||||||
private lateinit var statsJob: Job
|
private lateinit var statsJob: Job
|
||||||
|
|
||||||
private var config: Config? = null
|
|
||||||
|
|
||||||
private var backend: Backend = userspaceBackend
|
private var backend: Backend = userspaceBackend
|
||||||
|
|
||||||
private var backendIsUserspace = true
|
private var backendIsUserspace = true
|
||||||
|
|
||||||
init {
|
init {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
settingsRepository.getSettingsFlow().collect {
|
appDataRepository.settings.getSettingsFlow().collect {
|
||||||
if (it.isKernelEnabled && backendIsUserspace) {
|
if (it.isKernelEnabled && backendIsUserspace) {
|
||||||
Timber.d("Setting kernel backend")
|
Timber.d("Setting kernel backend")
|
||||||
backend = kernelBackend
|
backend = kernelBackend
|
||||||
|
@ -58,20 +55,21 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig): State {
|
override suspend fun startTunnel(tunnelConfig: TunnelConfig?): State {
|
||||||
return try {
|
return try {
|
||||||
stopTunnelOnConfigChange(tunnelConfig)
|
//TODO we need better error handling here
|
||||||
emitTunnelName(tunnelConfig.name)
|
val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel()
|
||||||
config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
if (config != null) {
|
||||||
emitTunnelConfig(config)
|
emitTunnelConfig(config)
|
||||||
|
val wgConfig = TunnelConfig.configFromQuick(config.wgQuick)
|
||||||
val state =
|
val state =
|
||||||
backend.setState(
|
backend.setState(
|
||||||
this,
|
this,
|
||||||
State.UP,
|
State.UP,
|
||||||
config,
|
wgConfig,
|
||||||
)
|
)
|
||||||
emitTunnelState(state)
|
|
||||||
state
|
state
|
||||||
|
} else throw Exception("No tunnels")
|
||||||
} catch (e: BackendException) {
|
} catch (e: BackendException) {
|
||||||
Timber.e("Failed to start tunnel with error: ${e.message}")
|
Timber.e("Failed to start tunnel with error: ${e.message}")
|
||||||
State.DOWN
|
State.DOWN
|
||||||
|
@ -94,32 +92,14 @@ constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun emitTunnelName(name: String) {
|
private suspend fun emitTunnelConfig(tunnelConfig: TunnelConfig?) {
|
||||||
_vpnState.emit(
|
_vpnState.emit(
|
||||||
_vpnState.value.copy(
|
_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() {
|
override suspend fun stopTunnel() {
|
||||||
try {
|
try {
|
||||||
if (getState() == State.UP) {
|
if (getState() == State.UP) {
|
||||||
|
@ -135,10 +115,14 @@ constructor(
|
||||||
return backend.getState(this)
|
return backend.getState(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return _vpnState.value.tunnelConfig?.name ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
override fun onStateChange(state: State) {
|
override fun onStateChange(state: State) {
|
||||||
val tunnel = this
|
val tunnel = this
|
||||||
emitTunnelState(state)
|
emitTunnelState(state)
|
||||||
WireGuardAutoTunnel.requestTileServiceStateUpdate(WireGuardAutoTunnel.instance)
|
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance)
|
||||||
if (state == State.UP) {
|
if (state == State.UP) {
|
||||||
statsJob =
|
statsJob =
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
|
|
@ -13,7 +13,6 @@ import com.zaneschepke.logcatter.Logcatter
|
||||||
import com.zaneschepke.logcatter.model.LogMessage
|
import com.zaneschepke.logcatter.model.LogMessage
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
@ -21,7 +20,6 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -35,27 +33,30 @@ constructor(
|
||||||
|
|
||||||
val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance)
|
val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance)
|
||||||
|
|
||||||
private val _appUiState = MutableStateFlow(AppUiState(
|
private val _appUiState = MutableStateFlow(
|
||||||
vpnPermissionAccepted = vpnIntent == null
|
AppUiState(
|
||||||
))
|
vpnPermissionAccepted = vpnIntent == null,
|
||||||
|
),
|
||||||
|
)
|
||||||
val appUiState = _appUiState.asStateFlow()
|
val appUiState = _appUiState.asStateFlow()
|
||||||
|
|
||||||
|
|
||||||
fun isRequiredPermissionGranted(): Boolean {
|
fun isRequiredPermissionGranted(): Boolean {
|
||||||
val allAccepted = (_appUiState.value.vpnPermissionAccepted && _appUiState.value.vpnPermissionAccepted)
|
val allAccepted =
|
||||||
|
(_appUiState.value.vpnPermissionAccepted && _appUiState.value.vpnPermissionAccepted)
|
||||||
if (!allAccepted) requestPermissions()
|
if (!allAccepted) requestPermissions()
|
||||||
return allAccepted
|
return allAccepted
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requestPermissions() {
|
private fun requestPermissions() {
|
||||||
_appUiState.value = _appUiState.value.copy(
|
_appUiState.value = _appUiState.value.copy(
|
||||||
requestPermissions = true
|
requestPermissions = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun permissionsRequested() {
|
fun permissionsRequested() {
|
||||||
_appUiState.value = _appUiState.value.copy(
|
_appUiState.value = _appUiState.value.copy(
|
||||||
requestPermissions = false
|
requestPermissions = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +75,7 @@ constructor(
|
||||||
|
|
||||||
fun onVpnPermissionAccepted() {
|
fun onVpnPermissionAccepted() {
|
||||||
_appUiState.value = _appUiState.value.copy(
|
_appUiState.value = _appUiState.value.copy(
|
||||||
vpnPermissionAccepted = true
|
vpnPermissionAccepted = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,28 +91,32 @@ constructor(
|
||||||
application.startActivity(
|
application.startActivity(
|
||||||
Intent.createChooser(intent, application.getString(R.string.email_chooser)).apply {
|
Intent.createChooser(intent, application.getString(R.string.email_chooser)).apply {
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
})
|
},
|
||||||
|
)
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
showSnackbarMessage(application.getString(R.string.no_email_detected))
|
showSnackbarMessage(application.getString(R.string.no_email_detected))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showSnackbarMessage(message: String) {
|
fun showSnackbarMessage(message: String) {
|
||||||
_appUiState.value = _appUiState.value.copy(
|
_appUiState.value = _appUiState.value.copy(
|
||||||
snackbarMessage = message,
|
snackbarMessage = message,
|
||||||
snackbarMessageConsumed = false
|
snackbarMessageConsumed = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun snackbarMessageConsumed() {
|
fun snackbarMessageConsumed() {
|
||||||
_appUiState.value = _appUiState.value.copy(
|
_appUiState.value = _appUiState.value.copy(
|
||||||
snackbarMessage = "",
|
snackbarMessage = "",
|
||||||
snackbarMessageConsumed = true
|
snackbarMessageConsumed = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val logs = mutableStateListOf<LogMessage>()
|
val logs = mutableStateListOf<LogMessage>()
|
||||||
|
|
||||||
fun readLogCatOutput() = viewModelScope.launch(viewModelScope.coroutineContext + Dispatchers.IO) {
|
fun readLogCatOutput() =
|
||||||
|
viewModelScope.launch(viewModelScope.coroutineContext + Dispatchers.IO) {
|
||||||
launch {
|
launch {
|
||||||
Logcatter.logs {
|
Logcatter.logs {
|
||||||
logs.add(it)
|
logs.add(it)
|
||||||
|
@ -131,12 +136,13 @@ constructor(
|
||||||
val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt"
|
val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt"
|
||||||
val content = logs.joinToString(separator = "\n")
|
val content = logs.joinToString(separator = "\n")
|
||||||
FileUtils.saveFileToDownloads(application.applicationContext, content, fileName)
|
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) {
|
fun setNotificationPermissionAccepted(accepted: Boolean) {
|
||||||
_appUiState.value = _appUiState.value.copy(
|
_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.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.SnackbarResult
|
import androidx.compose.material3.SnackbarResult
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
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.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusProperties
|
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.common.prompt.CustomSnackBar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
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.pinlock.PinLockScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
||||||
|
@ -61,7 +58,6 @@ import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import xyz.teamgravity.pin_lock_compose.PinLock
|
|
||||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -74,6 +70,9 @@ class MainActivity : AppCompatActivity() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var settingsRepository: SettingsRepository
|
lateinit var settingsRepository: SettingsRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
@OptIn(
|
@OptIn(
|
||||||
ExperimentalPermissionsApi::class,
|
ExperimentalPermissionsApi::class,
|
||||||
)
|
)
|
||||||
|
@ -85,10 +84,10 @@ class MainActivity : AppCompatActivity() {
|
||||||
// load preferences into memory and init data
|
// load preferences into memory and init data
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
dataStoreManager.init()
|
dataStoreManager.init()
|
||||||
WireGuardAutoTunnel.requestTileServiceStateUpdate(this@MainActivity)
|
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(this@MainActivity)
|
||||||
val settings = settingsRepository.getSettings()
|
val settings = settingsRepository.getSettings()
|
||||||
if (settings.isAutoTunnelEnabled) {
|
if (settings.isAutoTunnelEnabled) {
|
||||||
ServiceManager.startWatcherService(application.applicationContext)
|
serviceManager.startWatcherService(application.applicationContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setContent {
|
setContent {
|
||||||
|
@ -101,7 +100,6 @@ class MainActivity : AppCompatActivity() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
||||||
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) else null
|
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) else null
|
||||||
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
val vpnActivityResultState =
|
val vpnActivityResultState =
|
||||||
|
@ -160,6 +158,8 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
snackbarHost = {
|
snackbarHost = {
|
||||||
SnackbarHost(snackbarHostState) { snackbarData: SnackbarData ->
|
SnackbarHost(snackbarHostState) { snackbarData: SnackbarData ->
|
||||||
|
@ -176,9 +176,11 @@ class MainActivity : AppCompatActivity() {
|
||||||
//TODO refactor
|
//TODO refactor
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.focusable()
|
.focusable()
|
||||||
.focusProperties { when(navBackStackEntry?.destination?.route) {
|
.focusProperties {
|
||||||
|
when (navBackStackEntry?.destination?.route) {
|
||||||
Screen.Lock.route -> Unit
|
Screen.Lock.route -> Unit
|
||||||
else -> up = focusRequester }
|
else -> up = focusRequester
|
||||||
|
}
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
BottomNavBar(
|
BottomNavBar(
|
||||||
|
@ -215,7 +217,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
SettingsScreen(
|
SettingsScreen(
|
||||||
appViewModel = appViewModel,
|
appViewModel = appViewModel,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
focusRequester = focusRequester
|
focusRequester = focusRequester,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(
|
composable(
|
||||||
|
@ -235,14 +237,28 @@ class MainActivity : AppCompatActivity() {
|
||||||
if (!id.isNullOrBlank()) {
|
if (!id.isNullOrBlank()) {
|
||||||
ConfigScreen(
|
ConfigScreen(
|
||||||
navController = navController,
|
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,
|
appViewModel = appViewModel,
|
||||||
focusRequester = focusRequester,
|
focusRequester = focusRequester,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable(Screen.Lock.route) {
|
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,
|
route = route,
|
||||||
icon = Icons.Rounded.QuestionMark,
|
icon = Icons.Rounded.QuestionMark,
|
||||||
)
|
)
|
||||||
|
|
||||||
data object Logs : Screen("support/logs")
|
data object Logs : Screen("support/logs")
|
||||||
}
|
}
|
||||||
|
|
||||||
data object Config : Screen("config")
|
data object Config : Screen("config")
|
||||||
data object Lock : Screen("lock")
|
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.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ClickableIconButton(
|
fun ClickableIconButton(
|
||||||
|
@ -31,7 +29,10 @@ fun ClickableIconButton(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = icon.name,
|
contentDescription = icon.name,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.size(ButtonDefaults.IconSize).weight(1f, false).clickable {
|
Modifier
|
||||||
|
.size(ButtonDefaults.IconSize)
|
||||||
|
.weight(1f, false)
|
||||||
|
.clickable {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
onIconClick()
|
onIconClick()
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,8 @@ fun RowListItem(
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.animateContentSize()
|
Modifier
|
||||||
|
.animateContentSize()
|
||||||
.clip(RoundedCornerShape(30.dp))
|
.clip(RoundedCornerShape(30.dp))
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = { onClick() },
|
onClick = { onClick() },
|
||||||
|
@ -43,7 +44,9 @@ fun RowListItem(
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 15.dp, vertical = 5.dp),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 15.dp, vertical = 5.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
|
@ -59,7 +62,8 @@ fun RowListItem(
|
||||||
statistics?.peers()?.forEach {
|
statistics?.peers()?.forEach {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth()
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
.padding(end = 10.dp, bottom = 10.dp, start = 10.dp),
|
.padding(end = 10.dp, bottom = 10.dp, start = 10.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
|
|
@ -73,7 +73,8 @@ fun SearchBar(onQuery: (queryString: String) -> Unit) {
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth()
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape),
|
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -21,11 +22,13 @@ fun ConfigurationToggle(
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(padding),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(padding),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
Text(label)
|
Text(label, textAlign = TextAlign.Start)
|
||||||
Switch(
|
Switch(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
|
|
|
@ -21,26 +21,32 @@ fun AuthorizationPrompt(onSuccess: () -> Unit, onFailure: () -> Unit, onError: (
|
||||||
onError("Biometrics not available")
|
onError("Biometrics not available")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
|
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
|
||||||
onError("Biometrics not created")
|
onError("Biometrics not created")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
|
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
|
||||||
onError("Biometric hardware not found")
|
onError("Biometric hardware not found")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
|
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
|
||||||
onError("Biometric security update required")
|
onError("Biometric security update required")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
|
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
|
||||||
onError("Biometrics not supported")
|
onError("Biometrics not supported")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
|
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
|
||||||
onError("Biometrics status unknown")
|
onError("Biometrics status unknown")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_SUCCESS -> true
|
BiometricManager.BIOMETRIC_SUCCESS -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,10 +20,8 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -35,7 +33,8 @@ fun CustomSnackBar(
|
||||||
Snackbar(
|
Snackbar(
|
||||||
containerColor = containerColor,
|
containerColor = containerColor,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth(
|
Modifier
|
||||||
|
.fillMaxWidth(
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f,
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f,
|
||||||
)
|
)
|
||||||
.padding(bottom = 100.dp),
|
.padding(bottom = 100.dp),
|
||||||
|
@ -45,7 +44,9 @@ fun CustomSnackBar(
|
||||||
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr,
|
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.width(IntrinsicSize.Max).height(IntrinsicSize.Min),
|
modifier = Modifier
|
||||||
|
.width(IntrinsicSize.Max)
|
||||||
|
.height(IntrinsicSize.Min),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Start,
|
horizontalArrangement = Arrangement.Start,
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -16,7 +16,10 @@ fun LoadingScreen() {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier.fillMaxSize().focusable().padding(),
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.focusable()
|
||||||
|
.padding(),
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() }
|
Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() }
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,9 +12,14 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LogTypeLabel(color : Color, content: @Composable () -> Unit,) {
|
fun LogTypeLabel(color: Color, content: @Composable () -> Unit) {
|
||||||
Box(
|
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()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import androidx.compose.ui.unit.sp
|
||||||
fun SectionTitle(title: String, padding: Dp) {
|
fun SectionTitle(title: String, padding: Dp) {
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Start,
|
||||||
style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.ExtraBold),
|
style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.ExtraBold),
|
||||||
modifier = Modifier.padding(padding, bottom = 5.dp, top = 5.dp),
|
modifier = Modifier.padding(padding, bottom = 5.dp, top = 5.dp),
|
||||||
)
|
)
|
||||||
|
|
|
@ -94,7 +94,7 @@ fun ConfigScreen(
|
||||||
focusRequester: FocusRequester,
|
focusRequester: FocusRequester,
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
appViewModel: AppViewModel,
|
appViewModel: AppViewModel,
|
||||||
id: String
|
tunnelId: String
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||||
|
@ -105,7 +105,7 @@ fun ConfigScreen(
|
||||||
|
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
LaunchedEffect(Unit) { viewModel.init(id) }
|
LaunchedEffect(Unit) { viewModel.init(tunnelId) }
|
||||||
|
|
||||||
LaunchedEffect(uiState.loading) {
|
LaunchedEffect(uiState.loading) {
|
||||||
if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
|
@ -173,8 +173,10 @@ fun ConfigScreen(
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f),
|
.fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f),
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier
|
Column(
|
||||||
.fillMaxWidth()) {
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
|
@ -273,7 +275,7 @@ fun ConfigScreen(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
checked =
|
checked =
|
||||||
(uiState.checkedPackageNames.contains(
|
(uiState.checkedPackageNames.contains(
|
||||||
pack.packageName
|
pack.packageName,
|
||||||
)),
|
)),
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
if (it) {
|
if (it) {
|
||||||
|
@ -323,6 +325,7 @@ fun ConfigScreen(
|
||||||
appViewModel.showSnackbarMessage(it.data.message)
|
appViewModel.showSnackbarMessage(it.data.message)
|
||||||
navController.navigate(Screen.Main.route)
|
navController.navigate(Screen.Main.route)
|
||||||
}
|
}
|
||||||
|
|
||||||
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -390,10 +393,10 @@ fun ConfigScreen(
|
||||||
.clickable { showAuthPrompt = true },
|
.clickable { showAuthPrompt = true },
|
||||||
value = uiState.interfaceProxy.privateKey,
|
value = uiState.interfaceProxy.privateKey,
|
||||||
visualTransformation =
|
visualTransformation =
|
||||||
if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated)
|
if ((tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated)
|
||||||
VisualTransformation.None
|
VisualTransformation.None
|
||||||
else PasswordVisualTransformation(),
|
else PasswordVisualTransformation(),
|
||||||
enabled = (id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
|
enabled = (tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
|
||||||
onValueChange = { value -> viewModel.onPrivateKeyChange(value) },
|
onValueChange = { value -> viewModel.onPrivateKeyChange(value) },
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|
|
@ -7,17 +7,14 @@ import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.wireguard.config.BadConfigException
|
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
import com.wireguard.config.Interface
|
import com.wireguard.config.Interface
|
||||||
import com.wireguard.config.ParseException
|
|
||||||
import com.wireguard.config.Peer
|
import com.wireguard.config.Peer
|
||||||
import com.wireguard.crypto.Key
|
import com.wireguard.crypto.Key
|
||||||
import com.wireguard.crypto.KeyPair
|
import com.wireguard.crypto.KeyPair
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
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.data.repository.TunnelConfigRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
|
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
|
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
@ -30,7 +27,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -40,8 +36,7 @@ class ConfigViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
private val tunnelConfigRepository: TunnelConfigRepository,
|
private val appDataRepository: AppDataRepository
|
||||||
private val settingsRepository: SettingsRepository,
|
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val packageManager = application.packageManager
|
private val packageManager = application.packageManager
|
||||||
|
@ -55,7 +50,8 @@ constructor(
|
||||||
val state =
|
val state =
|
||||||
if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
||||||
val tunnelConfig =
|
val tunnelConfig =
|
||||||
tunnelConfigRepository.getAll().firstOrNull { it.id.toString() == tunnelId }
|
appDataRepository.tunnels.getAll()
|
||||||
|
.firstOrNull { it.id.toString() == tunnelId }
|
||||||
if (tunnelConfig != null) {
|
if (tunnelConfig != null) {
|
||||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||||
val proxyPeers = config.peers.map { PeerProxy.from(it) }
|
val proxyPeers = config.peers.map { PeerProxy.from(it) }
|
||||||
|
@ -103,7 +99,7 @@ constructor(
|
||||||
fun onAddCheckedPackage(packageName: String) {
|
fun onAddCheckedPackage(packageName: String) {
|
||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
checkedPackageNames = _uiState.value.checkedPackageNames + packageName
|
checkedPackageNames = _uiState.value.checkedPackageNames + packageName,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,7 +110,7 @@ constructor(
|
||||||
fun onRemoveCheckedPackage(packageName: String) {
|
fun onRemoveCheckedPackage(packageName: String) {
|
||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
checkedPackageNames = _uiState.value.checkedPackageNames - packageName
|
checkedPackageNames = _uiState.value.checkedPackageNames - packageName,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,23 +144,13 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveConfig(tunnelConfig: TunnelConfig) =
|
private fun saveConfig(tunnelConfig: TunnelConfig) =
|
||||||
viewModelScope.launch { tunnelConfigRepository.save(tunnelConfig) }
|
viewModelScope.launch { appDataRepository.tunnels.save(tunnelConfig) }
|
||||||
|
|
||||||
private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) =
|
private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (tunnelConfig != null) {
|
if (tunnelConfig != null) {
|
||||||
saveConfig(tunnelConfig).join()
|
saveConfig(tunnelConfig).join()
|
||||||
WireGuardAutoTunnel.requestTileServiceStateUpdate(application)
|
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
|
||||||
updateSettingsDefaultTunnel(tunnelConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,7 +196,11 @@ constructor(
|
||||||
val wgInterface = buildInterfaceListFromProxyInterface()
|
val wgInterface = buildInterfaceListFromProxyInterface()
|
||||||
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
||||||
val tunnelConfig = when (uiState.value.tunnel) {
|
val tunnelConfig = when (uiState.value.tunnel) {
|
||||||
null -> TunnelConfig(name = _uiState.value.tunnelName, wgQuick = config.toWgQuickString())
|
null -> TunnelConfig(
|
||||||
|
name = _uiState.value.tunnelName,
|
||||||
|
wgQuick = config.toWgQuickString(),
|
||||||
|
)
|
||||||
|
|
||||||
else -> uiState.value.tunnel!!.copy(
|
else -> uiState.value.tunnel!!.copy(
|
||||||
name = _uiState.value.tunnelName,
|
name = _uiState.value.tunnelName,
|
||||||
wgQuick = config.toWgQuickString(),
|
wgQuick = config.toWgQuickString(),
|
||||||
|
@ -306,21 +296,21 @@ constructor(
|
||||||
fun onAddressesChanged(value: String) {
|
fun onAddressesChanged(value: String) {
|
||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value)
|
interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onListenPortChanged(value: String) {
|
fun onListenPortChanged(value: String) {
|
||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value)
|
interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDnsServersChanged(value: String) {
|
fun onDnsServersChanged(value: String) {
|
||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(
|
_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) {
|
private fun onInterfacePublicKeyChange(value: String) {
|
||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value)
|
interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPrivateKeyChange(value: String) {
|
fun onPrivateKeyChange(value: String) {
|
||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value)
|
interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value),
|
||||||
)
|
)
|
||||||
if (NumberUtils.isValidKey(value)) {
|
if (NumberUtils.isValidKey(value)) {
|
||||||
val pair = KeyPair(Key.fromBase64(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.Circle
|
||||||
import androidx.compose.material.icons.rounded.CopyAll
|
import androidx.compose.material.icons.rounded.CopyAll
|
||||||
import androidx.compose.material.icons.rounded.Delete
|
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.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.material.icons.rounded.Star
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
@ -123,7 +124,6 @@ fun MainScreen(
|
||||||
val sheetState = rememberModalBottomSheetState()
|
val sheetState = rememberModalBottomSheetState()
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
var showPrimaryChangeAlertDialog by remember { mutableStateOf(false) }
|
|
||||||
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
|
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
|
||||||
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
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) {
|
AnimatedVisibility(showDeleteTunnelAlertDialog) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { showDeleteTunnelAlertDialog = false },
|
onDismissRequest = { showDeleteTunnelAlertDialog = false },
|
||||||
|
@ -426,7 +402,7 @@ fun MainScreen(
|
||||||
icon,
|
icon,
|
||||||
icon.name,
|
icon.name,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(end = 10.dp)
|
.padding(end = 8.5.dp)
|
||||||
.size(25.dp),
|
.size(25.dp),
|
||||||
tint =
|
tint =
|
||||||
if (uiState.settings.isAutoTunnelPaused) Color.Gray
|
if (uiState.settings.isAutoTunnelPaused) Color.Gray
|
||||||
|
@ -462,7 +438,7 @@ fun MainScreen(
|
||||||
) { tunnel ->
|
) { tunnel ->
|
||||||
val leadingIconColor =
|
val leadingIconColor =
|
||||||
(if (
|
(if (
|
||||||
uiState.vpnState.name == tunnel.name &&
|
uiState.vpnState.tunnelConfig?.name == tunnel.name &&
|
||||||
uiState.vpnState.status == Tunnel.State.UP
|
uiState.vpnState.status == Tunnel.State.UP
|
||||||
) {
|
) {
|
||||||
uiState.vpnState.statistics
|
uiState.vpnState.statistics
|
||||||
|
@ -486,31 +462,31 @@ fun MainScreen(
|
||||||
val expanded = remember { mutableStateOf(false) }
|
val expanded = remember { mutableStateOf(false) }
|
||||||
RowListItem(
|
RowListItem(
|
||||||
icon = {
|
icon = {
|
||||||
if (uiState.settings.isTunnelConfigDefault(tunnel)) {
|
val circleIcon = Icons.Rounded.Circle
|
||||||
Icon(
|
val icon = if (tunnel.isPrimaryTunnel) {
|
||||||
Icons.Rounded.Star,
|
Icons.Rounded.Star
|
||||||
stringResource(R.string.status),
|
} else if (tunnel.isMobileDataTunnel) {
|
||||||
tint = leadingIconColor,
|
Icons.Rounded.Smartphone
|
||||||
modifier = Modifier
|
|
||||||
.padding(end = 10.dp)
|
|
||||||
.size(20.dp),
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
|
circleIcon
|
||||||
|
}
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Circle,
|
icon,
|
||||||
stringResource(R.string.status),
|
icon.name,
|
||||||
tint = leadingIconColor,
|
tint = leadingIconColor,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(end = 15.dp)
|
.padding(
|
||||||
.size(15.dp),
|
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 = {
|
onHold = {
|
||||||
if (
|
if (
|
||||||
(uiState.vpnState.status == Tunnel.State.UP) &&
|
(uiState.vpnState.status == Tunnel.State.UP) &&
|
||||||
(tunnel.name == uiState.vpnState.name)
|
(tunnel.name == uiState.vpnState.tunnelConfig?.name)
|
||||||
) {
|
) {
|
||||||
appViewModel.showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
appViewModel.showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
||||||
return@RowListItem
|
return@RowListItem
|
||||||
|
@ -522,7 +498,7 @@ fun MainScreen(
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
if (
|
if (
|
||||||
uiState.vpnState.status == Tunnel.State.UP &&
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
(uiState.vpnState.name == tunnel.name)
|
(uiState.vpnState.tunnelConfig?.name == tunnel.name)
|
||||||
) {
|
) {
|
||||||
expanded.value = !expanded.value
|
expanded.value = !expanded.value
|
||||||
}
|
}
|
||||||
|
@ -539,7 +515,6 @@ fun MainScreen(
|
||||||
!WireGuardAutoTunnel.isRunningOnAndroidTv()
|
!WireGuardAutoTunnel.isRunningOnAndroidTv()
|
||||||
) {
|
) {
|
||||||
Row {
|
Row {
|
||||||
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (
|
if (
|
||||||
|
@ -550,38 +525,18 @@ fun MainScreen(
|
||||||
Event.Message.AutoTunnelOffAction.message,
|
Event.Message.AutoTunnelOffAction.message,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
showPrimaryChangeAlertDialog = true
|
navController.navigate(
|
||||||
|
"${Screen.Option.route}/${selectedTunnel?.id}",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
val icon = Icons.Rounded.Star
|
val icon = Icons.Rounded.Settings
|
||||||
Icon(
|
Icon(
|
||||||
icon,
|
icon,
|
||||||
icon.name,
|
icon.name,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
if (
|
|
||||||
uiState.settings.isAutoTunnelEnabled &&
|
|
||||||
uiState.settings.isTunnelConfigDefault(
|
|
||||||
tunnel,
|
|
||||||
) &&
|
|
||||||
!uiState.settings.isAutoTunnelPaused
|
|
||||||
) {
|
|
||||||
appViewModel.showSnackbarMessage(
|
|
||||||
Event.Message.AutoTunnelOffAction.message,
|
|
||||||
)
|
|
||||||
} else
|
|
||||||
navController.navigate(
|
|
||||||
"${Screen.Config.route}/${selectedTunnel?.id}",
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
val icon = Icons.Rounded.Edit
|
|
||||||
Icon(icon, icon.name)
|
|
||||||
}
|
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.focusable(),
|
modifier = Modifier.focusable(),
|
||||||
onClick = { viewModel.onCopyTunnel(selectedTunnel) },
|
onClick = { viewModel.onCopyTunnel(selectedTunnel) },
|
||||||
|
@ -601,7 +556,7 @@ fun MainScreen(
|
||||||
val checked by remember {
|
val checked by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
(uiState.vpnState.status == Tunnel.State.UP &&
|
(uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
tunnel.name == uiState.vpnState.name)
|
tunnel.name == uiState.vpnState.tunnelConfig?.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!checked) expanded.value = false
|
if (!checked) expanded.value = false
|
||||||
|
@ -618,7 +573,6 @@ fun MainScreen(
|
||||||
)
|
)
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Row {
|
Row {
|
||||||
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (uiState.settings.isAutoTunnelEnabled) {
|
if (uiState.settings.isAutoTunnelEnabled) {
|
||||||
|
@ -627,23 +581,24 @@ fun MainScreen(
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
selectedTunnel = tunnel
|
selectedTunnel = tunnel
|
||||||
showPrimaryChangeAlertDialog = true
|
navController.navigate(
|
||||||
|
"${Screen.Option.route}/${selectedTunnel?.id}",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
val icon = Icons.Rounded.Star
|
val icon = Icons.Rounded.Settings
|
||||||
Icon(
|
Icon(
|
||||||
icon,
|
icon,
|
||||||
icon.name,
|
icon.name,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
onClick = {
|
onClick = {
|
||||||
if (
|
if (
|
||||||
uiState.vpnState.status == Tunnel.State.UP &&
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
(uiState.vpnState.name == tunnel.name)
|
(uiState.vpnState.tunnelConfig?.name == tunnel.name)
|
||||||
) {
|
) {
|
||||||
expanded.value = !expanded.value
|
expanded.value = !expanded.value
|
||||||
} else {
|
} else {
|
||||||
|
@ -656,25 +611,6 @@ fun MainScreen(
|
||||||
val icon = Icons.Rounded.Info
|
val icon = Icons.Rounded.Info
|
||||||
Icon(icon, icon.name)
|
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(
|
IconButton(
|
||||||
onClick = { viewModel.onCopyTunnel(tunnel) },
|
onClick = { viewModel.onCopyTunnel(tunnel) },
|
||||||
) {
|
) {
|
||||||
|
@ -685,7 +621,7 @@ fun MainScreen(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (
|
if (
|
||||||
uiState.vpnState.status == Tunnel.State.UP &&
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
tunnel.name == uiState.vpnState.name
|
tunnel.name == uiState.vpnState.tunnelConfig?.name
|
||||||
) {
|
) {
|
||||||
appViewModel.showSnackbarMessage(
|
appViewModel.showSnackbarMessage(
|
||||||
Event.Message.TunnelOffAction.message,
|
Event.Message.TunnelOffAction.message,
|
||||||
|
@ -699,7 +635,7 @@ fun MainScreen(
|
||||||
val icon = Icons.Rounded.Delete
|
val icon = Icons.Rounded.Delete
|
||||||
Icon(
|
Icon(
|
||||||
icon,
|
icon,
|
||||||
icon.name
|
icon.name,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
TunnelSwitch()
|
TunnelSwitch()
|
||||||
|
|
|
@ -7,13 +7,11 @@ import android.net.Uri
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.google.zxing.common.StringUtils
|
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
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.data.repository.TunnelConfigRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
@ -22,8 +20,6 @@ import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
@ -39,15 +35,15 @@ class MainViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
private val tunnelConfigRepository: TunnelConfigRepository,
|
private val appDataRepository: AppDataRepository,
|
||||||
private val settingsRepository: SettingsRepository,
|
private val serviceManager: ServiceManager,
|
||||||
private val vpnService: VpnService
|
val vpnService: VpnService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val uiState =
|
val uiState =
|
||||||
combine(
|
combine(
|
||||||
settingsRepository.getSettingsFlow(),
|
appDataRepository.settings.getSettingsFlow(),
|
||||||
tunnelConfigRepository.getTunnelConfigsFlow(),
|
appDataRepository.tunnels.getTunnelConfigsFlow(),
|
||||||
vpnService.vpnState,
|
vpnService.vpnState,
|
||||||
) { settings, tunnels, vpnState ->
|
) { settings, tunnels, vpnState ->
|
||||||
MainUiState(settings, tunnels, vpnState, false)
|
MainUiState(settings, tunnels, vpnState, false)
|
||||||
|
@ -60,48 +56,46 @@ constructor(
|
||||||
|
|
||||||
private fun stopWatcherService() =
|
private fun stopWatcherService() =
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
ServiceManager.stopWatcherService(application.applicationContext)
|
serviceManager.stopWatcherService(application.applicationContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDelete(tunnel: TunnelConfig) {
|
fun onDelete(tunnel: TunnelConfig) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val settings = settingsRepository.getSettings()
|
val settings = appDataRepository.settings.getSettings()
|
||||||
val isDefault = settings.isTunnelConfigDefault(tunnel)
|
val isPrimary = tunnel.isPrimaryTunnel
|
||||||
if (tunnelConfigRepository.count() == 1 || isDefault) {
|
if (appDataRepository.tunnels.count() == 1 || isPrimary) {
|
||||||
stopWatcherService()
|
stopWatcherService()
|
||||||
settings.defaultTunnel = null
|
resetTunnelSetting(settings)
|
||||||
settings.isAutoTunnelEnabled = false
|
|
||||||
settings.isAlwaysOnVpnEnabled = false
|
|
||||||
saveSettings(settings)
|
|
||||||
}
|
}
|
||||||
tunnelConfigRepository.delete(tunnel)
|
appDataRepository.tunnels.delete(tunnel)
|
||||||
WireGuardAutoTunnel.requestTileServiceStateUpdate(application)
|
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun resetTunnelSetting(settings: Settings) {
|
||||||
|
saveSettings(
|
||||||
|
settings.copy(
|
||||||
|
isAutoTunnelEnabled = false,
|
||||||
|
isAlwaysOnVpnEnabled = false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun onTunnelStart(tunnelConfig: TunnelConfig) =
|
fun onTunnelStart(tunnelConfig: TunnelConfig) =
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
Timber.d("On start called!")
|
Timber.d("On start called!")
|
||||||
stopActiveTunnel().await()
|
serviceManager.startVpnService(
|
||||||
startTunnel(tunnelConfig)
|
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() =
|
fun onTunnelStop() =
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
Timber.i("Stopping active tunnel")
|
Timber.i("Stopping active tunnel")
|
||||||
ServiceManager.stopVpnService(application.applicationContext)
|
serviceManager.stopVpnService(application.applicationContext, isManualStop = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validateConfigString(config: String) {
|
private fun validateConfigString(config: String) {
|
||||||
|
@ -145,6 +139,7 @@ constructor(
|
||||||
is Result.Success -> return it
|
is Result.Success -> return it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
|
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
|
||||||
else -> return Result.Error(Event.Error.InvalidFileExtension)
|
else -> return Result.Error(Event.Error.InvalidFileExtension)
|
||||||
}
|
}
|
||||||
|
@ -186,23 +181,25 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
||||||
val firstTunnel = tunnelConfigRepository.count() == 0
|
val firstTunnel = appDataRepository.tunnels.count() == 0
|
||||||
saveTunnel(tunnelConfig)
|
saveTunnel(tunnelConfig)
|
||||||
if(firstTunnel) WireGuardAutoTunnel.requestTileServiceStateUpdate(application)
|
if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pauseAutoTunneling() =
|
fun pauseAutoTunneling() =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
|
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
|
||||||
|
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resumeAutoTunneling() =
|
fun resumeAutoTunneling() =
|
||||||
viewModelScope.launch {
|
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) {
|
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
|
||||||
tunnelConfigRepository.save(tunnelConfig)
|
appDataRepository.tunnels.save(tunnelConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFileNameByCursor(context: Context, uri: Uri): String? {
|
private fun getFileNameByCursor(context: Context, uri: Uri): String? {
|
||||||
|
@ -252,20 +249,17 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveSettings(settings: Settings) =
|
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 {
|
fun onCopyTunnel(tunnel: TunnelConfig?) = viewModelScope.launch {
|
||||||
tunnel?.let {
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,11 +39,15 @@ fun PinLockScreen(navController: NavController, appViewModel: AppViewModel) {
|
||||||
},
|
},
|
||||||
onPinIncorrect = {
|
onPinIncorrect = {
|
||||||
// pin is incorrect, show error
|
// 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 = {
|
onPinCreated = {
|
||||||
// pin created for the first time, navigate or hide pin lock
|
// 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() {
|
fun handleAutoTunnelToggle() {
|
||||||
if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) {
|
if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) {
|
||||||
if (appViewModel.isRequiredPermissionGranted()) {
|
if (appViewModel.isRequiredPermissionGranted()) {
|
||||||
viewModel.toggleAutoTunnel()
|
viewModel.onToggleAutoTunnel()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
requestBatteryOptimizationsDisabled()
|
requestBatteryOptimizationsDisabled()
|
||||||
|
|
|
@ -7,10 +7,9 @@ import androidx.core.location.LocationManagerCompat
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.wireguard.android.util.RootShell
|
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.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
@ -29,26 +28,25 @@ class SettingsViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
private val tunnelConfigRepository: TunnelConfigRepository,
|
private val appDataRepository: AppDataRepository,
|
||||||
private val settingsRepository: SettingsRepository,
|
private val serviceManager: ServiceManager,
|
||||||
private val dataStoreManager: DataStoreManager,
|
|
||||||
private val rootShell: RootShell,
|
private val rootShell: RootShell,
|
||||||
private val vpnService: VpnService
|
vpnService: VpnService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val uiState =
|
val uiState =
|
||||||
combine(
|
combine(
|
||||||
settingsRepository.getSettingsFlow(),
|
appDataRepository.settings.getSettingsFlow(),
|
||||||
tunnelConfigRepository.getTunnelConfigsFlow(),
|
appDataRepository.tunnels.getTunnelConfigsFlow(),
|
||||||
vpnService.vpnState,
|
vpnService.vpnState,
|
||||||
dataStoreManager.preferencesFlow,
|
appDataRepository.appState.generalStateFlow,
|
||||||
) { settings, tunnels, tunnelState, preferences ->
|
) { settings, tunnels, tunnelState, generalState ->
|
||||||
SettingsUiState(
|
SettingsUiState(
|
||||||
settings,
|
settings,
|
||||||
tunnels,
|
tunnels,
|
||||||
tunnelState,
|
tunnelState,
|
||||||
preferences?.get(DataStoreManager.LOCATION_DISCLOSURE_SHOWN) ?: false,
|
generalState.locationDisclosureShown,
|
||||||
preferences?.get(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN) ?: false,
|
generalState.batteryOptimizationDisableShown,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.stateIn(
|
.stateIn(
|
||||||
|
@ -70,12 +68,12 @@ constructor(
|
||||||
|
|
||||||
fun setLocationDisclosureShown() =
|
fun setLocationDisclosureShown() =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, true)
|
appDataRepository.appState.setLocationDisclosureShown(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setBatteryOptimizeDisableShown() =
|
fun setBatteryOptimizeDisableShown() =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, true)
|
appDataRepository.appState.setBatteryOptimizationDisableShown(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleTunnelOnMobileData() {
|
fun onToggleTunnelOnMobileData() {
|
||||||
|
@ -95,43 +93,37 @@ constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getDefaultTunnelOrFirst(): String {
|
fun onToggleAutoTunnel() =
|
||||||
return uiState.value.settings.defaultTunnel
|
|
||||||
?: tunnelConfigRepository.getAll().first().toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toggleAutoTunnel() =
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
|
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
|
||||||
var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused
|
var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused
|
||||||
|
|
||||||
if (isAutoTunnelEnabled) {
|
if (isAutoTunnelEnabled) {
|
||||||
ServiceManager.stopWatcherService(application)
|
serviceManager.stopWatcherService(application)
|
||||||
} else {
|
} else {
|
||||||
ServiceManager.startWatcherService(application)
|
serviceManager.startWatcherService(application)
|
||||||
isAutoTunnelPaused = false
|
isAutoTunnelPaused = false
|
||||||
}
|
}
|
||||||
saveSettings(
|
saveSettings(
|
||||||
uiState.value.settings.copy(
|
uiState.value.settings.copy(
|
||||||
isAutoTunnelEnabled = !isAutoTunnelEnabled,
|
isAutoTunnelEnabled = !isAutoTunnelEnabled,
|
||||||
isAutoTunnelPaused = isAutoTunnelPaused,
|
isAutoTunnelPaused = isAutoTunnelPaused,
|
||||||
defaultTunnel = getDefaultTunnelOrFirst(),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleAlwaysOnVPN() =
|
fun onToggleAlwaysOnVPN() =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val updatedSettings =
|
saveSettings(
|
||||||
uiState.value.settings.copy(
|
uiState.value.settings.copy(
|
||||||
isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled,
|
isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled,
|
||||||
defaultTunnel = getDefaultTunnelOrFirst(),
|
),
|
||||||
)
|
)
|
||||||
saveSettings(updatedSettings)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveSettings(settings: Settings) =
|
private fun saveSettings(settings: Settings) =
|
||||||
viewModelScope.launch { settingsRepository.save(settings) }
|
viewModelScope.launch { appDataRepository.settings.save(settings) }
|
||||||
|
|
||||||
fun onToggleTunnelOnEthernet() {
|
fun onToggleTunnelOnEthernet() {
|
||||||
saveSettings(
|
saveSettings(
|
||||||
|
@ -154,14 +146,6 @@ constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleBatterySaver() {
|
|
||||||
saveSettings(
|
|
||||||
uiState.value.settings.copy(
|
|
||||||
isBatterySaverEnabled = !uiState.value.settings.isBatterySaverEnabled,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveKernelMode(on: Boolean) {
|
private fun saveKernelMode(on: Boolean) {
|
||||||
saveSettings(
|
saveSettings(
|
||||||
uiState.value.settings.copy(
|
uiState.value.settings.copy(
|
||||||
|
|
|
@ -50,7 +50,6 @@ import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SupportScreen(
|
fun SupportScreen(
|
||||||
|
@ -71,7 +70,7 @@ fun SupportScreen(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.focusable()
|
.focusable(),
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
|
@ -128,13 +127,13 @@ fun SupportScreen(
|
||||||
}
|
}
|
||||||
Icon(
|
Icon(
|
||||||
forwardIcon,
|
forwardIcon,
|
||||||
forwardIcon.name
|
forwardIcon.name,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = 0.5.dp,
|
thickness = 0.5.dp,
|
||||||
color = MaterialTheme.colorScheme.onBackground
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
)
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.discord_url)) },
|
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.discord_url)) },
|
||||||
|
@ -160,13 +159,13 @@ fun SupportScreen(
|
||||||
}
|
}
|
||||||
Icon(
|
Icon(
|
||||||
forwardIcon,
|
forwardIcon,
|
||||||
forwardIcon.name
|
forwardIcon.name,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = 0.5.dp,
|
thickness = 0.5.dp,
|
||||||
color = MaterialTheme.colorScheme.onBackground
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
)
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.github_url)) },
|
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.github_url)) },
|
||||||
|
@ -192,13 +191,13 @@ fun SupportScreen(
|
||||||
}
|
}
|
||||||
Icon(
|
Icon(
|
||||||
forwardIcon,
|
forwardIcon,
|
||||||
forwardIcon.name
|
forwardIcon.name,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = 0.5.dp,
|
thickness = 0.5.dp,
|
||||||
color = MaterialTheme.colorScheme.onBackground
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
)
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { appViewModel.launchEmail() },
|
onClick = { appViewModel.launchEmail() },
|
||||||
|
@ -220,14 +219,14 @@ fun SupportScreen(
|
||||||
}
|
}
|
||||||
Icon(
|
Icon(
|
||||||
forwardIcon,
|
forwardIcon,
|
||||||
forwardIcon.name
|
forwardIcon.name,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = 0.5.dp,
|
thickness = 0.5.dp,
|
||||||
color = MaterialTheme.colorScheme.onBackground
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
)
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { navController.navigate(Screen.Support.Logs.route) },
|
onClick = { navController.navigate(Screen.Support.Logs.route) },
|
||||||
|
@ -249,7 +248,7 @@ fun SupportScreen(
|
||||||
}
|
}
|
||||||
Icon(
|
Icon(
|
||||||
Icons.AutoMirrored.Rounded.ArrowForward,
|
Icons.AutoMirrored.Rounded.ArrowForward,
|
||||||
stringResource(id = R.string.go)
|
stringResource(id = R.string.go),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.stateIn
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SupportViewModel @Inject constructor(private val settingsRepository: SettingsRepository) :
|
class SupportViewModel @Inject constructor(settingsRepository: SettingsRepository) :
|
||||||
ViewModel() {
|
ViewModel() {
|
||||||
|
|
||||||
val uiState =
|
val uiState =
|
||||||
|
|
|
@ -31,7 +31,6 @@ import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel
|
import com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -61,16 +60,16 @@ fun LogsScreen(appViewModel: AppViewModel) {
|
||||||
appViewModel.saveLogsToFile()
|
appViewModel.saveLogsToFile()
|
||||||
},
|
},
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
containerColor = MaterialTheme.colorScheme.primary
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
) {
|
) {
|
||||||
val icon = Icons.Filled.Save
|
val icon = Icons.Filled.Save
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = icon.name,
|
contentDescription = icon.name,
|
||||||
tint = MaterialTheme.colorScheme.onPrimary
|
tint = MaterialTheme.colorScheme.onPrimary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
@ -78,9 +77,12 @@ fun LogsScreen(appViewModel: AppViewModel) {
|
||||||
state = lazyColumnListState,
|
state = lazyColumnListState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = 24.dp)) {
|
.padding(horizontal = 24.dp),
|
||||||
|
) {
|
||||||
items(logs) {
|
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
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clickable(
|
.clickable(
|
||||||
|
@ -88,13 +90,17 @@ fun LogsScreen(appViewModel: AppViewModel) {
|
||||||
indication = null,
|
indication = null,
|
||||||
onClick = {
|
onClick = {
|
||||||
clipboardManager.setText(annotatedString = AnnotatedString(it.toString()))
|
clipboardManager.setText(annotatedString = AnnotatedString(it.toString()))
|
||||||
}
|
},
|
||||||
)
|
),
|
||||||
) {
|
) {
|
||||||
val fontSize = 10.sp
|
val fontSize = 10.sp
|
||||||
Text(text = it.tag, modifier = Modifier.fillMaxSize(0.3f), fontSize = fontSize)
|
Text(text = it.tag, modifier = Modifier.fillMaxSize(0.3f), fontSize = fontSize)
|
||||||
LogTypeLabel(color = Color(it.level.color())) {
|
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)
|
Text("${it.message} - ${it.time}", fontSize = fontSize)
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,6 +57,7 @@ fun WireguardAutoTunnelTheme(
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
darkTheme -> DarkColorScheme
|
darkTheme -> DarkColorScheme
|
||||||
else -> LightColorScheme
|
else -> LightColorScheme
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ object Constants {
|
||||||
const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L
|
const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L
|
||||||
const val VPN_CONNECTED_NOTIFICATION_DELAY = 3_000L
|
const val VPN_CONNECTED_NOTIFICATION_DELAY = 3_000L
|
||||||
const val TOGGLE_TUNNEL_DELAY = 300L
|
const val TOGGLE_TUNNEL_DELAY = 300L
|
||||||
|
const val WATCHER_COLLECTION_DELAY = 1_000L
|
||||||
const val CONF_FILE_EXTENSION = ".conf"
|
const val CONF_FILE_EXTENSION = ".conf"
|
||||||
const val ZIP_FILE_EXTENSION = ".zip"
|
const val ZIP_FILE_EXTENSION = ".zip"
|
||||||
const val URI_CONTENT_SCHEME = "content"
|
const val URI_CONTENT_SCHEME = "content"
|
||||||
|
@ -17,6 +18,7 @@ object Constants {
|
||||||
const val TEXT_MIME_TYPE = "text/plain"
|
const val TEXT_MIME_TYPE = "text/plain"
|
||||||
const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs"
|
const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs"
|
||||||
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
||||||
|
const val ALWAYS_ON_VPN_ACTION = "android.net.VpnService"
|
||||||
const val EMAIL_MIME_TYPE = "message/rfc822"
|
const val EMAIL_MIME_TYPE = "message/rfc822"
|
||||||
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024
|
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024
|
||||||
|
|
||||||
|
@ -29,4 +31,8 @@ object Constants {
|
||||||
const val PING_INTERVAL = 60_000L
|
const val PING_INTERVAL = 60_000L
|
||||||
const val PING_COOLDOWN = PING_INTERVAL * 60 //one hour
|
const val PING_COOLDOWN = PING_INTERVAL * 60 //one hour
|
||||||
|
|
||||||
|
const val ALLOWED_DISPLAY_NAME_LENGTH = 20
|
||||||
|
|
||||||
|
const val TUNNEL_EXTRA_KEY = "tunnelId"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import java.util.zip.ZipOutputStream
|
||||||
object FileUtils {
|
object FileUtils {
|
||||||
private const val ZIP_FILE_MIME_TYPE = "application/zip"
|
private const val ZIP_FILE_MIME_TYPE = "application/zip"
|
||||||
|
|
||||||
|
//TODO issue with android 9
|
||||||
private fun createDownloadsFileOutputStream(
|
private fun createDownloadsFileOutputStream(
|
||||||
context: Context,
|
context: Context,
|
||||||
fileName: String,
|
fileName: String,
|
||||||
|
@ -47,9 +48,9 @@ object FileUtils {
|
||||||
fun saveFileToDownloads(context: Context, content: String, fileName: String) {
|
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 {
|
val contentValues = ContentValues().apply {
|
||||||
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
|
put(MediaColumns.DISPLAY_NAME, fileName)
|
||||||
put(MediaStore.MediaColumns.MIME_TYPE, Constants.TEXT_MIME_TYPE)
|
put(MediaColumns.MIME_TYPE, Constants.TEXT_MIME_TYPE)
|
||||||
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||||
}
|
}
|
||||||
val resolver = context.contentResolver
|
val resolver = context.contentResolver
|
||||||
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
||||||
|
@ -61,7 +62,7 @@ object FileUtils {
|
||||||
} else {
|
} else {
|
||||||
val target = File(
|
val target = File(
|
||||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||||
fileName
|
fileName,
|
||||||
)
|
)
|
||||||
FileOutputStream(target).use { output ->
|
FileOutputStream(target).use { output ->
|
||||||
output.write(content.toByteArray())
|
output.write(content.toByteArray())
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.util
|
package com.zaneschepke.wireguardautotunnel.util
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class ReleaseTree : Timber.Tree() {
|
class ReleaseTree : Timber.DebugTree() {
|
||||||
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
override fun d(t: Throwable?) {
|
||||||
when(priority) {
|
return
|
||||||
Log.DEBUG -> return
|
|
||||||
}
|
}
|
||||||
super.log(priority,tag,message,t)
|
|
||||||
|
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"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
|
android:tint="#FFFFFF"
|
||||||
android:viewportWidth="640"
|
android:viewportWidth="640"
|
||||||
android:viewportHeight="640"
|
android:viewportHeight="640">
|
||||||
android:tint="#FFFFFF">
|
<group
|
||||||
<group android:scaleX="1.0132159"
|
android:scaleX="1.0132159"
|
||||||
android:scaleY="1.0132159"
|
android:scaleY="1.0132159"
|
||||||
android:translateX="-4.229075"
|
android:translateX="-4.229075"
|
||||||
android:translateY="-4.229075">
|
android:translateY="-4.229075">
|
||||||
<path
|
<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:fillColor="#53bdb6"
|
||||||
android:fillType="evenOdd"
|
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" />
|
android:strokeColor="#00000000" />
|
||||||
</group>
|
</group>
|
||||||
</vector>
|
</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,42 +3,53 @@
|
||||||
android:height="180dp"
|
android:height="180dp"
|
||||||
android:viewportWidth="640"
|
android:viewportWidth="640"
|
||||||
android:viewportHeight="640">
|
android:viewportHeight="640">
|
||||||
<group android:scaleX="0.6666667"
|
<group
|
||||||
|
android:scaleX="0.6666667"
|
||||||
android:scaleY="0.6666667"
|
android:scaleY="0.6666667"
|
||||||
android:translateX="106.666664"
|
android:translateX="106.666664"
|
||||||
android:translateY="106.666664">
|
android:translateY="106.666664">
|
||||||
<group android:scaleX="0.315"
|
<group
|
||||||
|
android:scaleX="0.315"
|
||||||
android:scaleY="0.56"
|
android:scaleY="0.56"
|
||||||
android:translateX="46.4"
|
android:translateX="46.4"
|
||||||
android:translateY="140.8">
|
android:translateY="140.8">
|
||||||
<path
|
<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:fillColor="#53bdb6"
|
||||||
android:fillType="evenOdd"
|
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" />
|
android:strokeColor="#00000000" />
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
<group android:scaleX="0.52644"
|
<group
|
||||||
|
android:scaleX="0.52644"
|
||||||
android:scaleY="0.20707752"
|
android:scaleY="0.20707752"
|
||||||
android:translateX="320.4"
|
android:translateX="320.4"
|
||||||
android:translateY="70.26513">
|
android:translateY="70.26513">
|
||||||
<group android:translateY="132.92308">
|
<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"
|
<path
|
||||||
android:fillColor="#53BDB6"/>
|
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: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
|
||||||
<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"
|
||||||
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: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: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: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
|
||||||
<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"
|
||||||
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: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: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: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: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>
|
||||||
</group>
|
</group>
|
||||||
|
|
|
@ -3,14 +3,15 @@
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="640"
|
android:viewportWidth="640"
|
||||||
android:viewportHeight="640">
|
android:viewportHeight="640">
|
||||||
<group android:scaleX="0.6"
|
<group
|
||||||
|
android:scaleX="0.6"
|
||||||
android:scaleY="0.6"
|
android:scaleY="0.6"
|
||||||
android:translateX="128"
|
android:translateX="128"
|
||||||
android:translateY="128">
|
android:translateY="128">
|
||||||
<path
|
<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:fillColor="#53bdb6"
|
||||||
android:fillType="evenOdd"
|
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" />
|
android:strokeColor="#00000000" />
|
||||||
</group>
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">WG Tunnel</string>
|
<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_id">VPN Channel</string>
|
||||||
<string name="vpn_channel_name">VPN Notification Channel</string>
|
<string name="vpn_channel_name">VPN Notification Channel</string>
|
||||||
<string name="watcher_channel_id">Watcher 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_title">Watcher Service</string>
|
||||||
<string name="watcher_notification_text_active">Monitoring network state changes: active</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="watcher_notification_text_paused">Monitoring network state changes: paused</string>
|
||||||
<string name="tunnel_start_title">VPN Connected</string>
|
<string name="tunnel_start_title">VPN connected</string>
|
||||||
<string name="tunnel_start_text">Connected to tunnel -</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="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="notification_permission_required">Notifications permission required.</string>
|
||||||
<string name="open_settings">Open Settings</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_on">Primary VPN on</string>
|
||||||
<string name="default_vpn_off">Primary VPN off</string>
|
<string name="default_vpn_off">Primary VPN off</string>
|
||||||
<string name="create_import">Create from scratch</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="turn_on_tunnel">Action requires active tunnel</string>
|
||||||
<string name="add_peer">Add peer</string>
|
<string name="add_peer">Add peer</string>
|
||||||
<string name="done">Done</string>
|
<string name="done">Done</string>
|
||||||
|
@ -121,8 +120,6 @@
|
||||||
<string name="seconds">seconds</string>
|
<string name="seconds">seconds</string>
|
||||||
<string name="persistent_keepalive">Persistent keepalive</string>
|
<string name="persistent_keepalive">Persistent keepalive</string>
|
||||||
<string name="cancel">Cancel</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_authentication_failed">Authentication failed</string>
|
||||||
<string name="error_authorization_failed">Failed to authorize</string>
|
<string name="error_authorization_failed">Failed to authorize</string>
|
||||||
<string name="enabled_app_shortcuts">Enable app shortcuts</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="precise_location_required">Precise location required</string>
|
||||||
<string name="unknown_error">Unknown error occurred</string>
|
<string name="unknown_error">Unknown error occurred</string>
|
||||||
<string name="exported_configs_message">Exported configs to downloads</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="tunnel_on_wifi">Tunnel on untrusted wifi</string>
|
||||||
<string name="my_email" translatable="false">support@zaneschepke.com</string>
|
<string name="my_email" translatable="false">support@zaneschepke.com</string>
|
||||||
<string name="email_subject">WG Tunnel Support</string>
|
<string name="email_subject">WG Tunnel Support</string>
|
||||||
|
@ -179,5 +175,17 @@
|
||||||
<string name="enter_pin">Enter your pin</string>
|
<string name="enter_pin">Enter your pin</string>
|
||||||
<string name="create_pin">Create pin</string>
|
<string name="create_pin">Create pin</string>
|
||||||
<string name="enable_app_lock">Enabled app lock</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>
|
</resources>
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<style name="Theme.WireguardAutoTunnel" parent="@style/Theme.AppCompat.NoActionBar">
|
<style name="Theme.WireguardAutoTunnel" parent="@style/Theme.AppCompat.NoActionBar">
|
||||||
<item name="android:windowBackground">@color/black_background</item>
|
<item name="android:windowBackground">@color/black_background</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -33,4 +33,36 @@
|
||||||
</intent>
|
</intent>
|
||||||
<capability-binding android:key="actions.intent.STOP" />
|
<capability-binding android:key="actions.intent.STOP" />
|
||||||
</shortcut>
|
</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>
|
</shortcuts>
|
||||||
|
|
|
@ -26,7 +26,8 @@ object BuildHelper {
|
||||||
val properties = java.util.Properties()
|
val properties = java.util.Properties()
|
||||||
val localProperties = File(file)
|
val localProperties = File(file)
|
||||||
if (localProperties.isFile) {
|
if (localProperties.isFile) {
|
||||||
java.io.InputStreamReader(java.io.FileInputStream(localProperties), Charsets.UTF_8).use { reader ->
|
java.io.InputStreamReader(java.io.FileInputStream(localProperties), Charsets.UTF_8)
|
||||||
|
.use { reader ->
|
||||||
properties.load(reader)
|
properties.load(reader)
|
||||||
}
|
}
|
||||||
} else return null
|
} else return null
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
object Constants {
|
object Constants {
|
||||||
const val VERSION_NAME = "3.3.9"
|
const val VERSION_NAME = "3.4.0"
|
||||||
const val JVM_TARGET = "17"
|
const val JVM_TARGET = "17"
|
||||||
const val VERSION_CODE = 33900
|
const val VERSION_CODE = 34000
|
||||||
const val TARGET_SDK = 34
|
const val TARGET_SDK = 34
|
||||||
const val MIN_SDK = 26
|
const val MIN_SDK = 26
|
||||||
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
||||||
const val APP_NAME = "wgtunnel"
|
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"
|
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"
|
timber = "5.0.1"
|
||||||
tunnel = "1.0.20230706"
|
tunnel = "1.0.20230706"
|
||||||
androidGradlePlugin = "8.3.1"
|
androidGradlePlugin = "8.3.1"
|
||||||
kotlin = "1.9.22"
|
kotlin = "1.9.23"
|
||||||
ksp = "1.9.22-1.0.17"
|
ksp = "1.9.23-1.0.19"
|
||||||
composeBom = "2024.02.02"
|
composeBom = "2024.03.00"
|
||||||
compose = "1.6.3"
|
compose = "1.6.4"
|
||||||
zxingAndroidEmbedded = "4.3.0"
|
zxingAndroidEmbedded = "4.3.0"
|
||||||
zxingCore = "3.5.3"
|
zxingCore = "3.5.3"
|
||||||
|
|
||||||
#plugins
|
#plugins
|
||||||
gradlePlugins-kotlinxSerialization = "1.8.21"
|
gradlePlugins-kotlinxSerialization = "1.9.23"
|
||||||
material = "1.11.0"
|
material = "1.11.0"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ android {
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = false
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
package com.zaneschepke.logcatter.model
|
package com.zaneschepke.logcatter.model
|
||||||
|
|
||||||
enum class LogLevel(val signifier: String) {
|
enum class LogLevel(val signifier: String) {
|
||||||
DEBUG("D") {
|
DEBUG("D") {
|
||||||
override fun color(): Long {
|
override fun color(): Long {
|
||||||
|
@ -32,6 +33,7 @@ enum class LogLevel(val signifier: String) {
|
||||||
};
|
};
|
||||||
|
|
||||||
abstract fun color(): Long
|
abstract fun color(): Long
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromSignifier(signifier: String): LogLevel {
|
fun fromSignifier(signifier: String): LogLevel {
|
||||||
return when (signifier) {
|
return when (signifier) {
|
||||||
|
|
|
@ -13,15 +13,30 @@ data class LogMessage(
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "$time $pid $tid $level $tag message= $message"
|
return "$time $pid $tid $level $tag message= $message"
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun from(logcatLine: String): LogMessage {
|
fun from(logcatLine: String): LogMessage {
|
||||||
return if(logcatLine.contains("---------")) LogMessage(Instant.now(), "0","0",LogLevel.VERBOSE,"System", logcatLine)
|
return if (logcatLine.contains("---------")) LogMessage(
|
||||||
|
Instant.now(),
|
||||||
|
"0",
|
||||||
|
"0",
|
||||||
|
LogLevel.VERBOSE,
|
||||||
|
"System",
|
||||||
|
logcatLine,
|
||||||
|
)
|
||||||
else {
|
else {
|
||||||
//TODO improve this
|
//TODO improve this
|
||||||
val parts = logcatLine.trim().split(" ").filter { it.isNotEmpty() }
|
val parts = logcatLine.trim().split(" ").filter { it.isNotEmpty() }
|
||||||
val epochParts = parts[0].split(".").map { it.toLong() }
|
val epochParts = parts[0].split(".").map { it.toLong() }
|
||||||
val message = parts.subList(5, parts.size).joinToString(" ")
|
val message = parts.subList(5, parts.size).joinToString(" ")
|
||||||
LogMessage(Instant.ofEpochSecond(epochParts[0], epochParts[1]), parts[1], parts[2], LogLevel.fromSignifier(parts[3]), parts[4], message)
|
LogMessage(
|
||||||
|
Instant.ofEpochSecond(epochParts[0], epochParts[1]),
|
||||||
|
parts[1],
|
||||||
|
parts[2],
|
||||||
|
LogLevel.fromSignifier(parts[3]),
|
||||||
|
parts[4],
|
||||||
|
message,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ dependencyResolutionManagement {
|
||||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
maven("https://gitea.zaneschepke.com/api/packages/zane/maven")
|
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
@ -21,7 +20,8 @@ fun getLocalProperty(key: String, file: String = "local.properties"): String? {
|
||||||
val properties = java.util.Properties()
|
val properties = java.util.Properties()
|
||||||
val localProperties = File(file)
|
val localProperties = File(file)
|
||||||
if (localProperties.isFile) {
|
if (localProperties.isFile) {
|
||||||
java.io.InputStreamReader(java.io.FileInputStream(localProperties), Charsets.UTF_8).use { reader ->
|
java.io.InputStreamReader(java.io.FileInputStream(localProperties), Charsets.UTF_8)
|
||||||
|
.use { reader ->
|
||||||
properties.load(reader)
|
properties.load(reader)
|
||||||
}
|
}
|
||||||
} else return null
|
} else return null
|
||||||
|
|