feat: androidtv navigation, auto-tunneling pause
Improved AndroidTV navigation to be less clunky and more streamlined Added auto-tunneling pause feature to UI to allow of quick auto tunneling pauses. App shortcuts and quick tile also override auto tunneling by engaging pause for temporary override of VPN purposes. Fixed bug where auto start on reboot was not working on older devices and AndroidTV. Fixed bug where location services is prefenting some flavors of Android from using auto-tunneling. Fixed bug where location permissions were not being detected correctly on AndroidTV versions. Fixed bug where quick tile could become out of sync. Improved notifications to show proper state of auto-tunneling and vpn. Removed excessive vibration from notifications. Improved error handling. Closes #75 Closes #73 Closes #61 Closes #53 Closes #30
This commit is contained in:
parent
f0ec661223
commit
aeb4a13389
|
@ -70,7 +70,7 @@ jobs:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
# fix hardcode changelog file name
|
# fix hardcode changelog file name
|
||||||
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/32500.txt
|
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/33000.txt
|
||||||
tag_name: ${{ github.ref_name }}
|
tag_name: ${{ github.ref_name }}
|
||||||
name: Release ${{ github.ref_name }}
|
name: Release ${{ github.ref_name }}
|
||||||
draft: false
|
draft: false
|
||||||
|
|
|
@ -15,6 +15,7 @@ android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = Constants.APP_ID
|
applicationId = Constants.APP_ID
|
||||||
minSdk = Constants.MIN_SDK
|
minSdk = Constants.MIN_SDK
|
||||||
|
compileSdk = Constants.COMPILE_SDK
|
||||||
targetSdk = Constants.TARGET_SDK
|
targetSdk = Constants.TARGET_SDK
|
||||||
versionCode = Constants.VERSION_CODE
|
versionCode = Constants.VERSION_CODE
|
||||||
versionName = Constants.VERSION_NAME
|
versionName = Constants.VERSION_NAME
|
||||||
|
|
|
@ -22,3 +22,5 @@
|
||||||
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||||
<fields>;
|
<fields>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,161 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 5,
|
||||||
|
"identityHash": "bc15003a44746e18b9c260ec49737089",
|
||||||
|
"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, `default_tunnel` TEXT, `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_battery_saver_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)",
|
||||||
|
"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": "defaultTunnel",
|
||||||
|
"columnName": "default_tunnel",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "isBatterySaverEnabled",
|
||||||
|
"columnName": "is_battery_saver_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"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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)",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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, 'bc15003a44746e18b9c260ec49737089')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,11 +3,11 @@ package com.zaneschepke.wireguardautotunnel
|
||||||
import androidx.room.testing.MigrationTestHelper
|
import androidx.room.testing.MigrationTestHelper
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.AppDatabase
|
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||||
import java.io.IOException
|
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class MigrationTest {
|
class MigrationTest {
|
||||||
|
@ -21,30 +21,36 @@ class MigrationTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun migrate2To3() {
|
fun migrate4To5() {
|
||||||
helper.createDatabase(dbName, 3).apply {
|
helper.createDatabase(dbName, 4).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(
|
execSQL(
|
||||||
"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, " +
|
"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_battery_saver_enabled," +
|
||||||
"is_tunnel_on_wifi_enabled)" +
|
"is_tunnel_on_wifi_enabled," +
|
||||||
" VALUES (" +
|
"is_kernel_enabled," +
|
||||||
"false," +
|
"is_restore_on_boot_enabled," +
|
||||||
"false," +
|
"is_multi_tunnel_enabled)" +
|
||||||
|
" VALUES " +
|
||||||
|
"('false'," +
|
||||||
|
"'false'," +
|
||||||
"'[trustedSSID1,trustedSSID2]'," +
|
"'[trustedSSID1,trustedSSID2]'," +
|
||||||
"'defaultTunnel'," +
|
"'defaultTunnel'," +
|
||||||
"false," +
|
"'false'," +
|
||||||
"false," +
|
"'false'," +
|
||||||
"false," +
|
"'false'," +
|
||||||
"false," +
|
"'false'," +
|
||||||
"false)"
|
"'false'," +
|
||||||
|
"'false'," +
|
||||||
|
"'false'," +
|
||||||
|
"'false')"
|
||||||
)
|
)
|
||||||
execSQL(
|
execSQL(
|
||||||
"INSERT INTO TunnelConfig (name, wg_quick)" +
|
"INSERT INTO TunnelConfig (name, wg_quick)" +
|
||||||
|
@ -56,7 +62,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, 4, true)
|
helper.runMigrationsAndValidate(dbName, 5, 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.
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,9 +126,13 @@
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
</service>
|
</service>
|
||||||
<receiver android:enabled="true" android:name=".receiver.BootReceiver"
|
<receiver android:enabled="true" android:name=".receiver.BootReceiver"
|
||||||
android:exported="true">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
|
||||||
|
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||||
|
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver android:exported="false" android:name=".receiver.NotificationActionReceiver"/>
|
<receiver android:exported="false" android:name=".receiver.NotificationActionReceiver"/>
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
package com.zaneschepke.wireguardautotunnel
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import java.math.BigDecimal
|
|
||||||
import java.text.DecimalFormat
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
fun BroadcastReceiver.goAsync(
|
|
||||||
context: CoroutineContext = EmptyCoroutineContext,
|
|
||||||
block: suspend CoroutineScope.() -> Unit
|
|
||||||
) {
|
|
||||||
val pendingResult = goAsync()
|
|
||||||
@OptIn(DelicateCoroutinesApi::class) // Must run globally; there's no teardown callback.
|
|
||||||
GlobalScope.launch(context) {
|
|
||||||
try {
|
|
||||||
block()
|
|
||||||
} finally {
|
|
||||||
pendingResult.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun BigDecimal.toThreeDecimalPlaceString(): String {
|
|
||||||
val df = DecimalFormat("#.###")
|
|
||||||
return df.format(this)
|
|
||||||
}
|
|
|
@ -1,29 +1,32 @@
|
||||||
package com.zaneschepke.wireguardautotunnel
|
package com.zaneschepke.wireguardautotunnel
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.ComponentName
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.service.quicksettings.TileService
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.datastore.DataStoreManager
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import java.io.IOException
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.io.IOException
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class WireGuardAutoTunnel : Application() {
|
class WireGuardAutoTunnel : Application() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var settingsRepo: SettingsDoa
|
lateinit var settingsRepository: SettingsRepository
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var dataStoreManager: DataStoreManager
|
lateinit var dataStoreManager: DataStoreManager
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
instance = this
|
||||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||||
initSettings()
|
initSettings()
|
||||||
with(ProcessLifecycleOwner.get()) {
|
with(ProcessLifecycleOwner.get()) {
|
||||||
|
@ -31,6 +34,7 @@ class WireGuardAutoTunnel : Application() {
|
||||||
try {
|
try {
|
||||||
// load preferences into memory
|
// load preferences into memory
|
||||||
dataStoreManager.init()
|
dataStoreManager.init()
|
||||||
|
requestTileServiceStateUpdate()
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Timber.e("Failed to load preferences")
|
Timber.e("Failed to load preferences")
|
||||||
}
|
}
|
||||||
|
@ -41,16 +45,20 @@ class WireGuardAutoTunnel : Application() {
|
||||||
private fun initSettings() {
|
private fun initSettings() {
|
||||||
with(ProcessLifecycleOwner.get()) {
|
with(ProcessLifecycleOwner.get()) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
if (settingsRepo.getAll().isEmpty()) {
|
if (settingsRepository.getAll().isEmpty()) {
|
||||||
settingsRepo.save(Settings())
|
settingsRepository.save(Settings())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun isRunningOnAndroidTv(context: Context): Boolean {
|
lateinit var instance: WireGuardAutoTunnel private set
|
||||||
return context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
fun isRunningOnAndroidTv(): Boolean {
|
||||||
|
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||||
|
}
|
||||||
|
fun requestTileServiceStateUpdate() {
|
||||||
|
TileService.requestListeningState(instance, ComponentName(instance, TunnelControlTile::class.java))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,29 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.repository
|
package com.zaneschepke.wireguardautotunnel.data
|
||||||
|
|
||||||
import androidx.room.AutoMigration
|
import androidx.room.AutoMigration
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [Settings::class, TunnelConfig::class],
|
entities = [Settings::class, TunnelConfig::class],
|
||||||
version = 4,
|
version = 5,
|
||||||
autoMigrations = [
|
autoMigrations = [
|
||||||
AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3), AutoMigration(
|
AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3), AutoMigration(
|
||||||
from = 3,
|
from = 3,
|
||||||
to = 4
|
to = 4
|
||||||
|
),AutoMigration(
|
||||||
|
from = 4,
|
||||||
|
to = 5
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
exportSchema = true
|
exportSchema = true
|
||||||
)
|
)
|
||||||
@TypeConverters(DatabaseListConverters::class)
|
@TypeConverters(DatabaseListConverters::class)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
abstract fun settingDao(): SettingsDoa
|
abstract fun settingDao(): SettingsDao
|
||||||
|
|
||||||
abstract fun tunnelConfigDoa(): TunnelConfigDao
|
abstract fun tunnelConfigDoa(): TunnelConfigDao
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.repository
|
package com.zaneschepke.wireguardautotunnel.data
|
||||||
|
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
|
@ -1,15 +1,15 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.repository
|
package com.zaneschepke.wireguardautotunnel.data
|
||||||
|
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Delete
|
import androidx.room.Delete
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface SettingsDoa {
|
interface SettingsDao {
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun save(t: Settings)
|
suspend fun save(t: Settings)
|
||||||
|
|
||||||
|
@ -22,6 +22,9 @@ interface SettingsDoa {
|
||||||
@Query("SELECT * FROM settings")
|
@Query("SELECT * FROM settings")
|
||||||
suspend fun getAll(): List<Settings>
|
suspend fun getAll(): List<Settings>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM settings LIMIT 1")
|
||||||
|
fun getSettingsFlow(): Flow<Settings>
|
||||||
|
|
||||||
@Query("SELECT * FROM settings")
|
@Query("SELECT * FROM settings")
|
||||||
fun getAllFlow(): Flow<MutableList<Settings>>
|
fun getAllFlow(): Flow<MutableList<Settings>>
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.repository
|
package com.zaneschepke.wireguardautotunnel.data
|
||||||
|
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Delete
|
import androidx.room.Delete
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
|
@ -1,4 +1,4 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.repository.datastore
|
package com.zaneschepke.wireguardautotunnel.data.datastore
|
||||||
import android.content.Context
|
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
|
||||||
|
@ -27,12 +27,12 @@ class DataStoreManager(private val context: Context) {
|
||||||
context.dataStore.edit {
|
context.dataStore.edit {
|
||||||
it[key] = value
|
it[key] = value
|
||||||
}
|
}
|
||||||
|
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map {
|
||||||
fun <T> getFromStore(key: Preferences.Key<T>) =
|
|
||||||
context.dataStore.data.map {
|
|
||||||
it[key]
|
it[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun <T> getFromStore(key: Preferences.Key<T>) = context.dataStore.data.first { it.contains(key) }[key]
|
||||||
|
|
||||||
val locationDisclosureFlow: Flow<Boolean?> = context.dataStore.data.map {
|
val locationDisclosureFlow: Flow<Boolean?> = context.dataStore.data.map {
|
||||||
it[LOCATION_DISCLOSURE_SHOWN]
|
it[LOCATION_DISCLOSURE_SHOWN]
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.repository.model
|
package com.zaneschepke.wireguardautotunnel.data.model
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
|
@ -36,7 +36,11 @@ data class Settings(
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_multi_tunnel_enabled",
|
name = "is_multi_tunnel_enabled",
|
||||||
defaultValue = "false"
|
defaultValue = "false"
|
||||||
) var isMultiTunnelEnabled: Boolean = false
|
) var isMultiTunnelEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "is_auto_tunnel_paused",
|
||||||
|
defaultValue = "false"
|
||||||
|
) var isAutoTunnelPaused: Boolean = false,
|
||||||
) {
|
) {
|
||||||
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean {
|
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean {
|
||||||
return if (defaultTunnel != null) {
|
return if (defaultTunnel != null) {
|
|
@ -1,13 +1,13 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.repository.model
|
package com.zaneschepke.wireguardautotunnel.data.model
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
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 java.io.InputStream
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
@Entity(indices = [Index(value = ["name"], unique = true)])
|
@Entity(indices = [Index(value = ["name"], unique = true)])
|
||||||
@Serializable
|
@Serializable
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface SettingsRepository {
|
||||||
|
suspend fun save(settings : Settings)
|
||||||
|
fun getSettingsFlow() : Flow<Settings>
|
||||||
|
|
||||||
|
suspend fun getSettings() : Settings
|
||||||
|
suspend fun getAll() : List<Settings>
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
class SettingsRepositoryImpl(private val settingsDoa: SettingsDao) : SettingsRepository {
|
||||||
|
|
||||||
|
override suspend fun save(settings: Settings) {
|
||||||
|
settingsDoa.save(settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSettingsFlow(): Flow<Settings> {
|
||||||
|
return settingsDoa.getSettingsFlow()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getSettings(): Settings {
|
||||||
|
return settingsDoa.getAll().firstOrNull() ?: Settings()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getAll(): List<Settings> {
|
||||||
|
return settingsDoa.getAll()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface TunnelConfigRepository {
|
||||||
|
|
||||||
|
fun getTunnelConfigsFlow() : Flow<TunnelConfigs>
|
||||||
|
suspend fun getAll() : TunnelConfigs
|
||||||
|
suspend fun save(tunnelConfig: TunnelConfig)
|
||||||
|
suspend fun delete(tunnelConfig: TunnelConfig)
|
||||||
|
suspend fun count() : Int
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ package com.zaneschepke.wireguardautotunnel.module
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.AppDatabase
|
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.module
|
package com.zaneschepke.wireguardautotunnel.module
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.AppDatabase
|
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.datastore.DataStoreManager
|
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepositoryImpl
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepositoryImpl
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
@ -17,16 +21,28 @@ import javax.inject.Singleton
|
||||||
class RepositoryModule {
|
class RepositoryModule {
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun provideSettingsRepository(appDatabase: AppDatabase): SettingsDoa {
|
fun provideSettingsDoa(appDatabase: AppDatabase): SettingsDao {
|
||||||
return appDatabase.settingDao()
|
return appDatabase.settingDao()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun provideTunnelConfigRepository(appDatabase: AppDatabase): TunnelConfigDao {
|
fun provideTunnelConfigDoa(appDatabase: AppDatabase): TunnelConfigDao {
|
||||||
return appDatabase.tunnelConfigDoa()
|
return appDatabase.tunnelConfigDoa()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao): TunnelConfigRepository {
|
||||||
|
return TunnelConfigRepositoryImpl(tunnelConfigDao)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun provideSettingsRepository(settingsDao: SettingsDao): SettingsRepository {
|
||||||
|
return SettingsRepositoryImpl(settingsDao)
|
||||||
|
}
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun providePreferencesDataStore(@ApplicationContext context: Context): DataStoreManager {
|
fun providePreferencesDataStore(@ApplicationContext context: Context): DataStoreManager {
|
||||||
|
|
|
@ -6,7 +6,7 @@ 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.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
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
|
||||||
|
@ -51,8 +51,8 @@ class TunnelModule {
|
||||||
fun provideVpnService(
|
fun provideVpnService(
|
||||||
@Userspace userspaceBackend: Backend,
|
@Userspace userspaceBackend: Backend,
|
||||||
@Kernel kernelBackend: Backend,
|
@Kernel kernelBackend: Backend,
|
||||||
settingsDoa: SettingsDoa
|
settingsRepository : SettingsRepository
|
||||||
): VpnService {
|
): VpnService {
|
||||||
return WireGuardTunnel(userspaceBackend, kernelBackend, settingsDoa)
|
return WireGuardTunnel(userspaceBackend, kernelBackend, settingsRepository)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,34 +3,23 @@ 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.goAsync
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.goAsync
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class BootReceiver : BroadcastReceiver() {
|
class BootReceiver : BroadcastReceiver() {
|
||||||
@Inject
|
|
||||||
lateinit var settingsRepo: SettingsDoa
|
|
||||||
|
|
||||||
override fun onReceive(
|
@Inject
|
||||||
context: Context,
|
lateinit var settingsRepository: SettingsRepository
|
||||||
intent: Intent
|
override fun onReceive(context: Context?, intent: Intent?) = goAsync {
|
||||||
) = goAsync {
|
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
|
||||||
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
|
if(settingsRepository.getSettings().isAutoTunnelEnabled) {
|
||||||
try {
|
ServiceManager.startWatcherServiceForeground(context!!)
|
||||||
val settings = settingsRepo.getAll()
|
|
||||||
if (settings.isNotEmpty()) {
|
|
||||||
val setting = settings.first()
|
|
||||||
if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) {
|
|
||||||
ServiceManager.startWatcherService(context, setting.defaultTunnel!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,33 +3,30 @@ 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.Constants
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.goAsync
|
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.goAsync
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class NotificationActionReceiver : BroadcastReceiver() {
|
class NotificationActionReceiver : BroadcastReceiver() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var settingsRepo: SettingsDoa
|
lateinit var settingsRepository: SettingsRepository
|
||||||
|
|
||||||
override fun onReceive(
|
override fun onReceive(
|
||||||
context: Context,
|
context: Context,
|
||||||
intent: Intent?
|
intent: Intent?
|
||||||
) = goAsync {
|
) = goAsync {
|
||||||
try {
|
try {
|
||||||
val settings = settingsRepo.getAll()
|
val settings = settingsRepository.getSettings()
|
||||||
if (settings.isNotEmpty()) {
|
if (settings.defaultTunnel != null) {
|
||||||
val setting = settings.first()
|
ServiceManager.stopVpnService(context)
|
||||||
if (setting.defaultTunnel != null) {
|
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||||
ServiceManager.stopVpnService(context)
|
ServiceManager.startVpnService(context, settings.defaultTunnel.toString())
|
||||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
|
||||||
ServiceManager.startVpnService(context, setting.defaultTunnel.toString())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
cancel()
|
cancel()
|
||||||
|
|
|
@ -89,35 +89,23 @@ object ServiceManager {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startWatcherServiceForeground(
|
fun startWatcherServiceForeground(
|
||||||
context: Context,
|
context: Context,
|
||||||
tunnelConfig: String
|
|
||||||
) {
|
) {
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.START,
|
Action.START_FOREGROUND,
|
||||||
context,
|
context,
|
||||||
WireGuardConnectivityWatcherService::class.java,
|
WireGuardConnectivityWatcherService::class.java
|
||||||
mapOf(
|
|
||||||
context
|
|
||||||
.getString(R.string.tunnel_extras_key) to
|
|
||||||
tunnelConfig
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startWatcherService(
|
fun startWatcherService(
|
||||||
context: Context,
|
context: Context
|
||||||
tunnelConfig: String
|
|
||||||
) {
|
) {
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.START,
|
Action.START,
|
||||||
context,
|
context,
|
||||||
WireGuardConnectivityWatcherService::class.java,
|
WireGuardConnectivityWatcherService::class.java
|
||||||
mapOf(
|
|
||||||
context
|
|
||||||
.getString(R.string.tunnel_extras_key) to
|
|
||||||
tunnelConfig
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,19 +116,4 @@ object ServiceManager {
|
||||||
WireGuardConnectivityWatcherService::class.java
|
WireGuardConnectivityWatcherService::class.java
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleWatcherServiceForeground(
|
|
||||||
context: Context,
|
|
||||||
tunnelConfig: String
|
|
||||||
) {
|
|
||||||
when (
|
|
||||||
getServiceState(
|
|
||||||
context,
|
|
||||||
WireGuardConnectivityWatcherService::class.java
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
ServiceState.STARTED -> stopWatcherService(context)
|
|
||||||
ServiceState.STOPPED -> startWatcherServiceForeground(context, tunnelConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,10 +10,9 @@ import android.os.SystemClock
|
||||||
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.wireguard.android.backend.Tunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
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
|
||||||
|
@ -21,315 +20,348 @@ import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
|
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class WireGuardConnectivityWatcherService : ForegroundService() {
|
class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
private val foregroundId = 122
|
private val foregroundId = 122
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var wifiService: NetworkService<WifiService>
|
||||||
lateinit var wifiService: NetworkService<WifiService>
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var mobileDataService: NetworkService<MobileDataService>
|
||||||
lateinit var mobileDataService: NetworkService<MobileDataService>
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var ethernetService: NetworkService<EthernetService>
|
||||||
lateinit var ethernetService: NetworkService<EthernetService>
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
lateinit var settingsRepo: SettingsDoa
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var notificationService: NotificationService
|
||||||
lateinit var notificationService: NotificationService
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var vpnService: VpnService
|
||||||
lateinit var vpnService: VpnService
|
|
||||||
|
|
||||||
private var isWifiConnected = false
|
private val networkEventsFlow = MutableStateFlow(WatcherState())
|
||||||
private var isEthernetConnected = false
|
data class WatcherState(
|
||||||
private var isMobileDataConnected = false
|
val isWifiConnected: Boolean = false,
|
||||||
private var currentNetworkSSID = ""
|
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 lateinit var setting: Settings
|
|
||||||
private lateinit var tunnelConfig: String
|
|
||||||
|
|
||||||
private var wakeLock: PowerManager.WakeLock? = null
|
private var wakeLock: PowerManager.WakeLock? = null
|
||||||
private val tag = this.javaClass.name
|
private val tag = this.javaClass.name
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
launchWatcherNotification()
|
if(settingsRepository.getSettings().isAutoTunnelPaused) {
|
||||||
} catch (e: Exception) {
|
launchWatcherPausedNotification()
|
||||||
Timber.e("Failed to start watcher service, not enough permissions")
|
} else launchWatcherNotification()
|
||||||
}
|
} catch (e: Exception) {
|
||||||
}
|
Timber.e("Failed to start watcher service, not enough permissions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startService(extras: Bundle?) {
|
||||||
|
super.startService(extras)
|
||||||
|
try {
|
||||||
|
// we need this lock so our service gets not affected by Doze Mode
|
||||||
|
lifecycleScope.launch { initWakeLock() }
|
||||||
|
cancelWatcherJob()
|
||||||
|
startWatcherJob()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e("Failed to launch watcher service, no permissions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stopService(extras: Bundle?) {
|
||||||
|
super.stopService(extras)
|
||||||
|
wakeLock?.let {
|
||||||
|
if (it.isHeld) {
|
||||||
|
it.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cancelWatcherJob()
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchWatcherNotification(description: String = getString(R.string.watcher_notification_text_active)) {
|
||||||
|
val notification =
|
||||||
|
notificationService.createNotification(
|
||||||
|
channelId = getString(R.string.watcher_channel_id),
|
||||||
|
channelName = getString(R.string.watcher_channel_name),
|
||||||
|
title = getString(R.string.auto_tunnel_title),
|
||||||
|
description = description)
|
||||||
|
ServiceCompat.startForeground(
|
||||||
|
this, foregroundId, notification, Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchWatcherPausedNotification() {
|
||||||
|
launchWatcherNotification(getString(R.string.watcher_notification_text_paused))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun startService(extras: Bundle?) {
|
// TODO could this be restarting service in a bad state?
|
||||||
super.startService(extras)
|
// try to start task again if killed
|
||||||
try {
|
override fun onTaskRemoved(rootIntent: Intent) {
|
||||||
launchWatcherNotification()
|
Timber.d("Task Removed called")
|
||||||
val tunnelId = extras?.getString(getString(R.string.tunnel_extras_key))
|
val restartServiceIntent = Intent(rootIntent)
|
||||||
if (tunnelId != null) {
|
val restartServicePendingIntent: PendingIntent =
|
||||||
this.tunnelConfig = tunnelId
|
PendingIntent.getService(
|
||||||
}
|
|
||||||
// we need this lock so our service gets not affected by Doze Mode
|
|
||||||
lifecycleScope.launch {
|
|
||||||
initWakeLock()
|
|
||||||
}
|
|
||||||
cancelWatcherJob()
|
|
||||||
if (this::tunnelConfig.isInitialized) {
|
|
||||||
startWatcherJob()
|
|
||||||
} else {
|
|
||||||
stopService(extras)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e("Failed to launch watcher service, no permissions")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun stopService(extras: Bundle?) {
|
|
||||||
super.stopService(extras)
|
|
||||||
wakeLock?.let {
|
|
||||||
if (it.isHeld) {
|
|
||||||
it.release()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cancelWatcherJob()
|
|
||||||
stopSelf()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchWatcherNotification() {
|
|
||||||
val notification =
|
|
||||||
notificationService.createNotification(
|
|
||||||
channelId = getString(R.string.watcher_channel_id),
|
|
||||||
channelName = getString(R.string.watcher_channel_name),
|
|
||||||
description = getString(R.string.watcher_notification_text),
|
|
||||||
vibration = false
|
|
||||||
)
|
|
||||||
ServiceCompat.startForeground(
|
|
||||||
this,
|
this,
|
||||||
foregroundId,
|
1,
|
||||||
notification,
|
restartServiceIntent,
|
||||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID
|
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE)
|
||||||
)
|
applicationContext.getSystemService(Context.ALARM_SERVICE)
|
||||||
}
|
val alarmService: AlarmManager =
|
||||||
|
applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||||
|
alarmService.set(
|
||||||
|
AlarmManager.ELAPSED_REALTIME,
|
||||||
|
SystemClock.elapsedRealtime() + 1000,
|
||||||
|
restartServicePendingIntent)
|
||||||
|
}
|
||||||
|
|
||||||
// try to start task again if killed
|
private suspend fun initWakeLock() {
|
||||||
override fun onTaskRemoved(rootIntent: Intent) {
|
val isBatterySaverOn =
|
||||||
Timber.d("Task Removed called")
|
withContext(lifecycleScope.coroutineContext) {
|
||||||
val restartServiceIntent = Intent(rootIntent)
|
settingsRepository.getSettings().isBatterySaverEnabled
|
||||||
val restartServicePendingIntent: PendingIntent =
|
}
|
||||||
PendingIntent.getService(
|
wakeLock =
|
||||||
this,
|
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||||
1,
|
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
||||||
restartServiceIntent,
|
if (isBatterySaverOn) {
|
||||||
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
|
Timber.d("Initiating wakelock with timeout")
|
||||||
|
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
|
||||||
|
} else {
|
||||||
|
Timber.d("Initiating wakelock with zero timeout")
|
||||||
|
acquire(Constants.DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancelWatcherJob() {
|
||||||
|
if (this::watcherJob.isInitialized) {
|
||||||
|
watcherJob.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startWatcherJob() {
|
||||||
|
watcherJob =
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val setting = settingsRepository.getSettings()
|
||||||
|
launch {
|
||||||
|
Timber.d("Starting wifi watcher")
|
||||||
|
watchForWifiConnectivityChanges()
|
||||||
|
}
|
||||||
|
if (setting.isTunnelOnMobileDataEnabled) {
|
||||||
|
launch {
|
||||||
|
Timber.d("Starting mobile data watcher")
|
||||||
|
watchForMobileDataConnectivityChanges()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (setting.isTunnelOnEthernetEnabled) {
|
||||||
|
launch {
|
||||||
|
Timber.d("Starting ethernet data watcher")
|
||||||
|
watchForEthernetConnectivityChanges()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
Timber.d("Starting vpn state watcher")
|
||||||
|
watchForVpnConnectivityChanges()
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
Timber.d("Starting settings watcher")
|
||||||
|
watchForSettingsChanges()
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
Timber.d("Starting management watcher")
|
||||||
|
manageVpn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun watchForMobileDataConnectivityChanges() {
|
||||||
|
mobileDataService.networkStatus.collect {
|
||||||
|
when (it) {
|
||||||
|
is NetworkStatus.Available -> {
|
||||||
|
Timber.d("Gained Mobile data connection")
|
||||||
|
networkEventsFlow.value = networkEventsFlow.value.copy(
|
||||||
|
isMobileDataConnected = true
|
||||||
)
|
)
|
||||||
applicationContext.getSystemService(Context.ALARM_SERVICE)
|
}
|
||||||
val alarmService: AlarmManager =
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
networkEventsFlow.value = networkEventsFlow.value.copy(
|
||||||
alarmService.set(
|
isMobileDataConnected = true
|
||||||
AlarmManager.ELAPSED_REALTIME,
|
)
|
||||||
SystemClock.elapsedRealtime() + 1000,
|
Timber.d("Mobile data capabilities changed")
|
||||||
restartServicePendingIntent
|
}
|
||||||
)
|
is NetworkStatus.Unavailable -> {
|
||||||
|
networkEventsFlow.value = networkEventsFlow.value.copy(
|
||||||
|
isMobileDataConnected = false
|
||||||
|
)
|
||||||
|
Timber.d("Lost mobile data connection")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private suspend fun initWakeLock() {
|
private suspend fun watchForSettingsChanges() {
|
||||||
val isBatterySaverOn =
|
settingsRepository.getSettingsFlow().collect {
|
||||||
withContext(lifecycleScope.coroutineContext) {
|
if(networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
|
||||||
settingsRepo.getAll().firstOrNull()?.isBatterySaverEnabled ?: false
|
when(it.isAutoTunnelPaused) {
|
||||||
}
|
true -> launchWatcherPausedNotification()
|
||||||
wakeLock =
|
false -> launchWatcherNotification()
|
||||||
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
|
||||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
|
||||||
if (isBatterySaverOn) {
|
|
||||||
Timber.d("Initiating wakelock with timeout")
|
|
||||||
acquire(Constants.WATCHER_SERVICE_WAKE_LOCK_TIMEOUT)
|
|
||||||
} else {
|
|
||||||
Timber.d("Initiating wakelock with zero timeout")
|
|
||||||
acquire()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
networkEventsFlow.value = networkEventsFlow.value.copy(
|
||||||
|
settings = it
|
||||||
private fun cancelWatcherJob() {
|
)
|
||||||
if (this::watcherJob.isInitialized) {
|
|
||||||
watcherJob.cancel()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startWatcherJob() {
|
private suspend fun watchForVpnConnectivityChanges() {
|
||||||
watcherJob =
|
vpnService.vpnState.collect {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
when(it.status) {
|
||||||
val settings = settingsRepo.getAll()
|
Tunnel.State.DOWN -> networkEventsFlow.value = networkEventsFlow.value.copy(
|
||||||
if (settings.isNotEmpty()) {
|
isVpnConnected = false
|
||||||
setting = settings[0]
|
)
|
||||||
}
|
Tunnel.State.UP -> networkEventsFlow.value = networkEventsFlow.value.copy(
|
||||||
launch {
|
isVpnConnected = true
|
||||||
watchForWifiConnectivityChanges()
|
)
|
||||||
}
|
else -> {}
|
||||||
if (setting.isTunnelOnMobileDataEnabled) {
|
|
||||||
launch {
|
|
||||||
watchForMobileDataConnectivityChanges()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (setting.isTunnelOnEthernetEnabled) {
|
|
||||||
launch {
|
|
||||||
watchForEthernetConnectivityChanges()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
launch {
|
|
||||||
manageVpn()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun watchForMobileDataConnectivityChanges() {
|
|
||||||
mobileDataService.networkStatus.collect {
|
|
||||||
when (it) {
|
|
||||||
is NetworkStatus.Available -> {
|
|
||||||
Timber.d("Gained Mobile data connection")
|
|
||||||
isMobileDataConnected = true
|
|
||||||
}
|
|
||||||
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
|
||||||
isMobileDataConnected = true
|
|
||||||
Timber.d("Mobile data capabilities changed")
|
|
||||||
}
|
|
||||||
|
|
||||||
is NetworkStatus.Unavailable -> {
|
|
||||||
isMobileDataConnected = false
|
|
||||||
Timber.d("Lost mobile data connection")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun watchForEthernetConnectivityChanges() {
|
private suspend fun watchForEthernetConnectivityChanges() {
|
||||||
ethernetService.networkStatus.collect {
|
ethernetService.networkStatus.collect {
|
||||||
when (it) {
|
when (it) {
|
||||||
is NetworkStatus.Available -> {
|
is NetworkStatus.Available -> {
|
||||||
Timber.d("Gained Ethernet connection")
|
Timber.d("Gained Ethernet connection")
|
||||||
isEthernetConnected = true
|
networkEventsFlow.value = networkEventsFlow.value.copy(
|
||||||
}
|
isEthernetConnected = true
|
||||||
|
)
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
|
||||||
Timber.d("Ethernet capabilities changed")
|
|
||||||
isEthernetConnected = true
|
|
||||||
}
|
|
||||||
|
|
||||||
is NetworkStatus.Unavailable -> {
|
|
||||||
isEthernetConnected = false
|
|
||||||
Timber.d("Lost Ethernet connection")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
|
Timber.d("Ethernet capabilities changed")
|
||||||
private suspend fun watchForWifiConnectivityChanges() {
|
networkEventsFlow.value = networkEventsFlow.value.copy(
|
||||||
wifiService.networkStatus.collect {
|
isEthernetConnected = true
|
||||||
when (it) {
|
)
|
||||||
is NetworkStatus.Available -> {
|
|
||||||
Timber.d("Gained Wi-Fi connection")
|
|
||||||
isWifiConnected = true
|
|
||||||
}
|
|
||||||
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
|
||||||
Timber.d("Wifi capabilities changed")
|
|
||||||
isWifiConnected = true
|
|
||||||
val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: ""
|
|
||||||
Timber.d("Detected SSID: $ssid")
|
|
||||||
currentNetworkSSID = ssid
|
|
||||||
}
|
|
||||||
|
|
||||||
is NetworkStatus.Unavailable -> {
|
|
||||||
isWifiConnected = false
|
|
||||||
Timber.d("Lost Wi-Fi connection")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
is NetworkStatus.Unavailable -> {
|
||||||
|
networkEventsFlow.value = networkEventsFlow.value.copy(
|
||||||
|
isEthernetConnected = false
|
||||||
|
)
|
||||||
|
Timber.d("Lost Ethernet connection")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun manageVpn() {
|
private suspend fun watchForWifiConnectivityChanges() {
|
||||||
while (true) {
|
wifiService.networkStatus.collect {
|
||||||
|
when (it) {
|
||||||
|
is NetworkStatus.Available -> {
|
||||||
|
Timber.d("Gained Wi-Fi connection")
|
||||||
|
networkEventsFlow.value = networkEventsFlow.value.copy(
|
||||||
|
isWifiConnected = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
|
Timber.d("Wifi capabilities changed")
|
||||||
|
networkEventsFlow.value = networkEventsFlow.value.copy(
|
||||||
|
isWifiConnected = true
|
||||||
|
)
|
||||||
|
val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: ""
|
||||||
|
Timber.d("Detected SSID: $ssid")
|
||||||
|
networkEventsFlow.value = networkEventsFlow.value.copy(
|
||||||
|
currentNetworkSSID = ssid
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is NetworkStatus.Unavailable -> {
|
||||||
|
networkEventsFlow.value = networkEventsFlow.value.copy(
|
||||||
|
isWifiConnected = false
|
||||||
|
)
|
||||||
|
Timber.d("Lost Wi-Fi connection")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO clean this up
|
||||||
|
private suspend fun manageVpn() {
|
||||||
|
networkEventsFlow.collectLatest {
|
||||||
|
Timber.i("New watcher state: $it")
|
||||||
|
if (!it.settings.isAutoTunnelPaused && it.settings.defaultTunnel != null) {
|
||||||
|
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||||
when {
|
when {
|
||||||
(
|
((it.isEthernetConnected &&
|
||||||
(
|
it.settings.isTunnelOnEthernetEnabled &&
|
||||||
isEthernetConnected &&
|
!it.isVpnConnected)) -> {
|
||||||
setting.isTunnelOnEthernetEnabled &&
|
ServiceManager.startVpnService(this, it.settings.defaultTunnel!!)
|
||||||
vpnService.getState() == Tunnel.State.DOWN
|
Timber.i("Condition 1 met")
|
||||||
)
|
}
|
||||||
) ->
|
(!it.isEthernetConnected &&
|
||||||
ServiceManager.startVpnService(this, tunnelConfig)
|
it.settings.isTunnelOnMobileDataEnabled &&
|
||||||
|
!it.isWifiConnected &&
|
||||||
(
|
it.isMobileDataConnected &&
|
||||||
!isEthernetConnected &&
|
!it.isVpnConnected) -> {
|
||||||
setting.isTunnelOnMobileDataEnabled &&
|
ServiceManager.startVpnService(this, it.settings.defaultTunnel!!)
|
||||||
!isWifiConnected &&
|
Timber.i("Condition 2 met")
|
||||||
isMobileDataConnected &&
|
}
|
||||||
vpnService.getState() == Tunnel.State.DOWN
|
(!it.isEthernetConnected &&
|
||||||
) ->
|
!it.settings.isTunnelOnMobileDataEnabled &&
|
||||||
ServiceManager.startVpnService(this, tunnelConfig)
|
!it.isWifiConnected &&
|
||||||
|
it.isVpnConnected) -> {
|
||||||
(
|
|
||||||
!isEthernetConnected &&
|
|
||||||
!setting.isTunnelOnMobileDataEnabled &&
|
|
||||||
!isWifiConnected &&
|
|
||||||
vpnService.getState() == Tunnel.State.UP
|
|
||||||
) ->
|
|
||||||
ServiceManager.stopVpnService(this)
|
ServiceManager.stopVpnService(this)
|
||||||
|
Timber.i("Condition 3 met")
|
||||||
(
|
}
|
||||||
!isEthernetConnected && isWifiConnected &&
|
(!it.isEthernetConnected &&
|
||||||
!setting.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
|
it.isWifiConnected &&
|
||||||
setting.isTunnelOnWifiEnabled &&
|
!it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID) &&
|
||||||
(vpnService.getState() != Tunnel.State.UP)
|
it.settings.isTunnelOnWifiEnabled &&
|
||||||
) ->
|
(!it.isVpnConnected)) -> {
|
||||||
ServiceManager.startVpnService(this, tunnelConfig)
|
ServiceManager.startVpnService(this, it.settings.defaultTunnel!!)
|
||||||
|
Timber.i("Condition 4 met")
|
||||||
(
|
}
|
||||||
!isEthernetConnected && (
|
(!it.isEthernetConnected &&
|
||||||
isWifiConnected &&
|
(it.isWifiConnected && it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) &&
|
||||||
setting.trustedNetworkSSIDs.contains(currentNetworkSSID)
|
(it.isVpnConnected)) -> {
|
||||||
) &&
|
|
||||||
(vpnService.getState() == Tunnel.State.UP)
|
|
||||||
) ->
|
|
||||||
ServiceManager.stopVpnService(this)
|
ServiceManager.stopVpnService(this)
|
||||||
|
Timber.i("Condition 5 met")
|
||||||
(
|
}
|
||||||
!isEthernetConnected && (
|
(!it.isEthernetConnected &&
|
||||||
isWifiConnected &&
|
(it.isWifiConnected &&
|
||||||
!setting.isTunnelOnWifiEnabled &&
|
!it.settings.isTunnelOnWifiEnabled &&
|
||||||
(vpnService.getState() == Tunnel.State.UP)
|
(it.isVpnConnected))) -> {
|
||||||
)
|
|
||||||
) ->
|
|
||||||
ServiceManager.stopVpnService(this)
|
ServiceManager.stopVpnService(this)
|
||||||
|
Timber.i("Condition 6 met")
|
||||||
(
|
}
|
||||||
!isEthernetConnected && !isWifiConnected &&
|
(!it.isEthernetConnected &&
|
||||||
!isMobileDataConnected &&
|
!it.isWifiConnected &&
|
||||||
(vpnService.getState() == Tunnel.State.UP)
|
!it.isMobileDataConnected &&
|
||||||
) ->
|
(it.isVpnConnected)) -> {
|
||||||
ServiceManager.stopVpnService(this)
|
ServiceManager.stopVpnService(this)
|
||||||
|
Timber.i("Condition 7 met")
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// Do nothing
|
Timber.i("No condition met")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delay(Constants.VPN_CONNECTIVITY_CHECK_INTERVAL)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,20 +5,24 @@ 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.zaneschepke.wireguardautotunnel.Constants
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
|
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
|
||||||
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
|
||||||
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.handshakeStatus
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class WireGuardTunnelService : ForegroundService() {
|
class WireGuardTunnelService : ForegroundService() {
|
||||||
|
@ -28,7 +32,10 @@ class WireGuardTunnelService : ForegroundService() {
|
||||||
lateinit var vpnService: VpnService
|
lateinit var vpnService: VpnService
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var settingsRepo: SettingsDoa
|
lateinit var settingsRepository: SettingsRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var notificationService: NotificationService
|
lateinit var notificationService: NotificationService
|
||||||
|
@ -36,26 +43,29 @@ class WireGuardTunnelService : ForegroundService() {
|
||||||
private lateinit var job: Job
|
private lateinit var job: Job
|
||||||
|
|
||||||
private var tunnelName: String = ""
|
private var tunnelName: String = ""
|
||||||
|
private var didShowConnected = false
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
launchVpnStartingNotification()
|
if(tunnelConfigRepository.getAll().isNotEmpty()) {
|
||||||
|
launchVpnNotification()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun startService(extras: Bundle?) {
|
override fun startService(extras: Bundle?) {
|
||||||
super.startService(extras)
|
super.startService(extras)
|
||||||
// TODO fix grapheneOS calls always-on on install
|
|
||||||
launchVpnStartingNotification()
|
|
||||||
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
|
|
||||||
cancelJob()
|
cancelJob()
|
||||||
job =
|
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
val tunnelConfig = tunnelConfigString?.let {
|
||||||
|
TunnelConfig.from(it)
|
||||||
|
}
|
||||||
|
tunnelName = tunnelConfig?.name ?: ""
|
||||||
|
job = lifecycleScope.launch(Dispatchers.IO) {
|
||||||
launch {
|
launch {
|
||||||
if (tunnelConfigString != null) {
|
if (tunnelConfig != null) {
|
||||||
try {
|
try {
|
||||||
val tunnelConfig = TunnelConfig.from(tunnelConfigString)
|
|
||||||
tunnelName = tunnelConfig.name
|
tunnelName = tunnelConfig.name
|
||||||
vpnService.startTunnel(tunnelConfig)
|
vpnService.startTunnel(tunnelConfig)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -63,52 +73,45 @@ class WireGuardTunnelService : ForegroundService() {
|
||||||
stopService(extras)
|
stopService(extras)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Timber.d("Tunnel config null, starting default tunnel")
|
Timber.d("Tunnel config null, starting default tunnel or first")
|
||||||
val settings = settingsRepo.getAll()
|
val settings = settingsRepository.getSettings()
|
||||||
if (settings.isNotEmpty()) {
|
val tunnels = tunnelConfigRepository.getAll()
|
||||||
val setting = settings[0]
|
if (settings.isAlwaysOnVpnEnabled) {
|
||||||
if (setting.defaultTunnel != null && setting.isAlwaysOnVpnEnabled) {
|
val tunnel = if(settings.defaultTunnel != null) {
|
||||||
val tunnelConfig = TunnelConfig.from(setting.defaultTunnel!!)
|
TunnelConfig.from(settings.defaultTunnel!!)
|
||||||
tunnelName = tunnelConfig.name
|
} else if(tunnels.isNotEmpty()) {
|
||||||
vpnService.startTunnel(tunnelConfig)
|
tunnels.first()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
if(tunnel != null) {
|
||||||
|
tunnelName = tunnel.name
|
||||||
|
vpnService.startTunnel(tunnel)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//TODO add failed to connect notification
|
||||||
launch {
|
launch {
|
||||||
var didShowConnected = false
|
vpnService.vpnState.collect { state ->
|
||||||
var didShowFailedHandshakeNotification = false
|
state.statistics
|
||||||
vpnService.handshakeStatus.collect {
|
?.mapPeerStats()
|
||||||
when (it) {
|
?.map { it.value?.handshakeStatus() }
|
||||||
HandshakeStatus.NOT_STARTED -> {
|
.let { statuses ->
|
||||||
}
|
when {
|
||||||
HandshakeStatus.NEVER_CONNECTED -> {
|
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
|
||||||
if (!didShowFailedHandshakeNotification) {
|
if(!didShowConnected){
|
||||||
launchVpnConnectionFailedNotification(
|
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
|
||||||
getString(R.string.initial_connection_failure_message)
|
launchVpnNotification(getString(R.string.tunnel_start_title),"${getString(R.string.tunnel_start_text)} $tunnelName")
|
||||||
)
|
didShowConnected = true
|
||||||
didShowFailedHandshakeNotification = true
|
}
|
||||||
didShowConnected = false
|
}
|
||||||
|
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
|
||||||
|
statuses?.all { it == HandshakeStatus.NOT_STARTED } == true -> {}
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HandshakeStatus.HEALTHY -> {
|
|
||||||
if (!didShowConnected) {
|
|
||||||
launchVpnConnectedNotification()
|
|
||||||
didShowConnected = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
HandshakeStatus.STALE -> {}
|
|
||||||
HandshakeStatus.UNHEALTHY -> {
|
|
||||||
if (!didShowFailedHandshakeNotification) {
|
|
||||||
launchVpnConnectionFailedNotification(
|
|
||||||
getString(R.string.lost_connection_failure_message)
|
|
||||||
)
|
|
||||||
didShowFailedHandshakeNotification = true
|
|
||||||
didShowConnected = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -118,40 +121,22 @@ class WireGuardTunnelService : ForegroundService() {
|
||||||
super.stopService(extras)
|
super.stopService(extras)
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
vpnService.stopTunnel()
|
vpnService.stopTunnel()
|
||||||
|
didShowConnected = false
|
||||||
}
|
}
|
||||||
cancelJob()
|
cancelJob()
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchVpnConnectedNotification() {
|
private fun launchVpnNotification(title : String = getString(R.string.vpn_starting),description : String = getString(R.string.attempt_connection)) {
|
||||||
val notification =
|
val notification =
|
||||||
notificationService.createNotification(
|
notificationService.createNotification(
|
||||||
channelId = getString(R.string.vpn_channel_id),
|
channelId = getString(R.string.vpn_channel_id),
|
||||||
channelName = getString(R.string.vpn_channel_name),
|
channelName = getString(R.string.vpn_channel_name),
|
||||||
title = getString(R.string.tunnel_start_title),
|
title = title,
|
||||||
onGoing = false,
|
onGoing = false,
|
||||||
vibration = false,
|
vibration = false,
|
||||||
showTimestamp = true,
|
showTimestamp = true,
|
||||||
description = "${getString(R.string.tunnel_start_text)} $tunnelName"
|
description = description
|
||||||
)
|
|
||||||
ServiceCompat.startForeground(
|
|
||||||
this,
|
|
||||||
foregroundId,
|
|
||||||
notification,
|
|
||||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchVpnStartingNotification() {
|
|
||||||
val notification =
|
|
||||||
notificationService.createNotification(
|
|
||||||
channelId = getString(R.string.vpn_channel_id),
|
|
||||||
channelName = getString(R.string.vpn_channel_name),
|
|
||||||
title = getString(R.string.vpn_starting),
|
|
||||||
onGoing = false,
|
|
||||||
vibration = false,
|
|
||||||
showTimestamp = true,
|
|
||||||
description = getString(R.string.attempt_connection)
|
|
||||||
)
|
)
|
||||||
ServiceCompat.startForeground(
|
ServiceCompat.startForeground(
|
||||||
this,
|
this,
|
||||||
|
|
|
@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.service.notification
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
|
||||||
interface NotificationService {
|
interface NotificationService {
|
||||||
fun createNotification(
|
fun createNotification(
|
||||||
|
@ -16,6 +17,7 @@ interface NotificationService {
|
||||||
importance: Int = NotificationManager.IMPORTANCE_HIGH,
|
importance: Int = NotificationManager.IMPORTANCE_HIGH,
|
||||||
vibration: Boolean = false,
|
vibration: Boolean = false,
|
||||||
onGoing: Boolean = true,
|
onGoing: Boolean = true,
|
||||||
lights: Boolean = true
|
lights: Boolean = true,
|
||||||
|
onlyAlertOnce: Boolean = true,
|
||||||
): Notification
|
): Notification
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.MainActivity
|
import com.zaneschepke.wireguardautotunnel.ui.MainActivity
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
@ -20,6 +21,16 @@ constructor(
|
||||||
private val notificationManager =
|
private val notificationManager =
|
||||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
|
private val watcherBuilder: NotificationCompat.Builder =
|
||||||
|
NotificationCompat.Builder(
|
||||||
|
context,
|
||||||
|
context.getString(R.string.watcher_channel_id)
|
||||||
|
)
|
||||||
|
private val tunnelBuilder: NotificationCompat.Builder = NotificationCompat.Builder(
|
||||||
|
context,
|
||||||
|
context.getString(R.string.vpn_channel_id)
|
||||||
|
)
|
||||||
|
|
||||||
override fun createNotification(
|
override fun createNotification(
|
||||||
channelId: String,
|
channelId: String,
|
||||||
channelName: String,
|
channelName: String,
|
||||||
|
@ -31,7 +42,8 @@ constructor(
|
||||||
importance: Int,
|
importance: Int,
|
||||||
vibration: Boolean,
|
vibration: Boolean,
|
||||||
onGoing: Boolean,
|
onGoing: Boolean,
|
||||||
lights: Boolean
|
lights: Boolean,
|
||||||
|
onlyAlertOnce: Boolean,
|
||||||
): Notification {
|
): Notification {
|
||||||
val channel =
|
val channel =
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
|
@ -43,7 +55,7 @@ constructor(
|
||||||
it.enableLights(lights)
|
it.enableLights(lights)
|
||||||
it.lightColor = Color.RED
|
it.lightColor = Color.RED
|
||||||
it.enableVibration(vibration)
|
it.enableVibration(vibration)
|
||||||
it.vibrationPattern = longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)
|
it.vibrationPattern = longArrayOf(100,200,300)
|
||||||
it
|
it
|
||||||
}
|
}
|
||||||
notificationManager.createNotificationChannel(channel)
|
notificationManager.createNotificationChannel(channel)
|
||||||
|
@ -57,24 +69,31 @@ constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val builder: Notification.Builder =
|
val builder = when(channelId) {
|
||||||
Notification.Builder(
|
context.getString(R.string.watcher_channel_id) -> watcherBuilder
|
||||||
context,
|
context.getString(R.string.vpn_channel_id) -> tunnelBuilder
|
||||||
channelId
|
else -> {
|
||||||
)
|
NotificationCompat.Builder(
|
||||||
|
context,
|
||||||
|
channelId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return builder.let {
|
return builder.let {
|
||||||
if (action != null && actionText != null) {
|
if (action != null && actionText != null) {
|
||||||
// TODO find a not deprecated way to do this
|
|
||||||
it.addAction(
|
it.addAction(
|
||||||
Notification.Action.Builder(0, actionText, action)
|
NotificationCompat.Action.Builder(0, actionText, action)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
it.setAutoCancel(true)
|
it.setAutoCancel(true)
|
||||||
}
|
}
|
||||||
it.setContentTitle(title)
|
it.setContentTitle(title)
|
||||||
.setContentText(description)
|
.setContentText(description)
|
||||||
|
.setOnlyAlertOnce(onlyAlertOnce)
|
||||||
.setContentIntent(pendingIntent)
|
.setContentIntent(pendingIntent)
|
||||||
.setOngoing(onGoing)
|
.setOngoing(onGoing)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
.setShowWhen(showTimestamp)
|
.setShowWhen(showTimestamp)
|
||||||
.setSmallIcon(R.mipmap.ic_launcher_foreground)
|
.setSmallIcon(R.mipmap.ic_launcher_foreground)
|
||||||
.build()
|
.build()
|
||||||
|
|
|
@ -1,61 +1,63 @@
|
||||||
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.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
|
||||||
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.WireGuardTunnelService
|
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
||||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ShortcutsActivity : ComponentActivity() {
|
class ShortcutsActivity : ComponentActivity() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var settingsRepo: SettingsDoa
|
lateinit var settingsRepository: SettingsRepository
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var tunnelConfigRepo: TunnelConfigDao
|
lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||||
|
|
||||||
private fun attemptWatcherServiceToggle(tunnelConfig: String) {
|
private suspend fun toggleWatcherServicePause() {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
val settings = settingsRepository.getSettings()
|
||||||
val settings = getSettings()
|
|
||||||
if (settings.isAutoTunnelEnabled) {
|
if (settings.isAutoTunnelEnabled) {
|
||||||
ServiceManager.toggleWatcherServiceForeground(this@ShortcutsActivity, tunnelConfig)
|
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)
|
if (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)
|
||||||
.equals(WireGuardTunnelService::class.java.simpleName)
|
.equals(WireGuardTunnelService::class.java.simpleName)
|
||||||
) {
|
) {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
val settings = getSettings()
|
val settings = settingsRepository.getSettings()
|
||||||
if (settings.isShortcutsEnabled) {
|
if (settings.isShortcutsEnabled) {
|
||||||
try {
|
try {
|
||||||
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
|
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
|
||||||
val tunnelConfig =
|
val tunnelConfig =
|
||||||
if (tunnelName != null) {
|
if (tunnelName != null) {
|
||||||
tunnelConfigRepo.getAll().firstOrNull { it.name == tunnelName }
|
tunnelConfigRepository.getAll().firstOrNull { it.name == tunnelName }
|
||||||
} else {
|
} else {
|
||||||
if (settings.defaultTunnel == null) {
|
if (settings.defaultTunnel == null) {
|
||||||
tunnelConfigRepo.getAll().first()
|
tunnelConfigRepository.getAll().first()
|
||||||
} else {
|
} else {
|
||||||
TunnelConfig.from(settings.defaultTunnel!!)
|
TunnelConfig.from(settings.defaultTunnel!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tunnelConfig ?: return@launch
|
tunnelConfig ?: return@launch
|
||||||
attemptWatcherServiceToggle(tunnelConfig.toString())
|
toggleWatcherServicePause()
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
Action.STOP.name -> ServiceManager.stopVpnService(
|
Action.STOP.name -> ServiceManager.stopVpnService(
|
||||||
this@ShortcutsActivity
|
this@ShortcutsActivity
|
||||||
|
@ -67,6 +69,7 @@ class ShortcutsActivity : ComponentActivity() {
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e.message)
|
Timber.e(e.message)
|
||||||
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,15 +77,6 @@ class ShortcutsActivity : ComponentActivity() {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getSettings(): Settings {
|
|
||||||
val settings = settingsRepo.getAll()
|
|
||||||
return if (settings.isNotEmpty()) {
|
|
||||||
settings.first()
|
|
||||||
} else {
|
|
||||||
throw WgTunnelException("Settings empty")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TUNNEL_NAME_EXTRA_KEY = "tunnelName"
|
const val TUNNEL_NAME_EXTRA_KEY = "tunnelName"
|
||||||
const val CLASS_NAME_EXTRA_KEY = "className"
|
const val CLASS_NAME_EXTRA_KEY = "className"
|
||||||
|
|
|
@ -4,51 +4,67 @@ import android.os.Build
|
||||||
import android.service.quicksettings.Tile
|
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.R
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
|
||||||
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
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class TunnelControlTile : TileService() {
|
class TunnelControlTile() : TileService() {
|
||||||
@Inject
|
|
||||||
lateinit var settingsRepo: SettingsDoa
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var configRepo: TunnelConfigDao
|
lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var settingsRepository: SettingsRepository
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var vpnService: VpnService
|
lateinit var vpnService: VpnService
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.Main)
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
private lateinit var job: Job
|
private var tunnelName : String? = null
|
||||||
|
|
||||||
override fun onStartListening() {
|
override fun onStartListening() {
|
||||||
job =
|
|
||||||
scope.launch {
|
|
||||||
updateTileState()
|
|
||||||
}
|
|
||||||
super.onStartListening()
|
super.onStartListening()
|
||||||
|
Timber.d("On start listening called")
|
||||||
|
scope.launch {
|
||||||
|
vpnService.vpnState.collect {
|
||||||
|
when(it.status) {
|
||||||
|
Tunnel.State.UP -> setActive()
|
||||||
|
Tunnel.State.DOWN -> setInactive()
|
||||||
|
else -> setInactive()
|
||||||
|
}
|
||||||
|
val tunnels = tunnelConfigRepository.getAll()
|
||||||
|
if(tunnels.isEmpty()) {
|
||||||
|
setUnavailable()
|
||||||
|
return@collect
|
||||||
|
}
|
||||||
|
tunnelName = it.name.ifBlank {
|
||||||
|
val settings = settingsRepository.getSettings()
|
||||||
|
if (settings.defaultTunnel != null) {
|
||||||
|
TunnelConfig.from(settings.defaultTunnel!!).name
|
||||||
|
} else tunnels.firstOrNull()?.name
|
||||||
|
}
|
||||||
|
setTileDescription(tunnelName ?: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
scope.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTileRemoved() {
|
override fun onTileRemoved() {
|
||||||
super.onTileRemoved()
|
super.onTileRemoved()
|
||||||
cancelJob()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,17 +73,15 @@ class TunnelControlTile : TileService() {
|
||||||
unlockAndRun {
|
unlockAndRun {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
val tunnel = determineTileTunnel()
|
val tunnelConfig = tunnelConfigRepository.getAll().first { it.name == tunnelName }
|
||||||
if (tunnel != null) {
|
toggleWatcherServicePause()
|
||||||
attemptWatcherServiceToggle(tunnel.toString())
|
if (vpnService.getState() == Tunnel.State.UP) {
|
||||||
if (vpnService.getState() == Tunnel.State.UP) {
|
ServiceManager.stopVpnService(this@TunnelControlTile)
|
||||||
ServiceManager.stopVpnService(this@TunnelControlTile)
|
} else {
|
||||||
} else {
|
ServiceManager.startVpnServiceForeground(
|
||||||
ServiceManager.startVpnServiceForeground(
|
this@TunnelControlTile,
|
||||||
this@TunnelControlTile,
|
tunnelConfig.toString()
|
||||||
tunnel.toString()
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e.message)
|
Timber.e(e.message)
|
||||||
|
@ -78,68 +92,31 @@ class TunnelControlTile : TileService() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun determineTileTunnel(): TunnelConfig? {
|
private fun toggleWatcherServicePause() {
|
||||||
var tunnelConfig: TunnelConfig? = null
|
|
||||||
val settings = settingsRepo.getAll()
|
|
||||||
if (settings.isNotEmpty()) {
|
|
||||||
val setting = settings.first()
|
|
||||||
tunnelConfig =
|
|
||||||
if (setting.defaultTunnel != null) {
|
|
||||||
TunnelConfig.from(setting.defaultTunnel!!)
|
|
||||||
} else {
|
|
||||||
val configs = configRepo.getAll()
|
|
||||||
val config =
|
|
||||||
if (configs.isNotEmpty()) {
|
|
||||||
configs.first()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
config
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tunnelConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun attemptWatcherServiceToggle(tunnelConfig: String) {
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val settings = settingsRepo.getAll()
|
val settings = settingsRepository.getSettings()
|
||||||
if (settings.isNotEmpty()) {
|
if (settings.isAutoTunnelEnabled) {
|
||||||
val setting = settings.first()
|
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
||||||
if (setting.isAutoTunnelEnabled) {
|
settingsRepository.save(settings.copy(
|
||||||
ServiceManager.toggleWatcherServiceForeground(
|
isAutoTunnelPaused = pauseAutoTunnel
|
||||||
this@TunnelControlTile,
|
))
|
||||||
tunnelConfig
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateTileState() {
|
private fun setActive() {
|
||||||
vpnService.state.collect {
|
qsTile.state = Tile.STATE_ACTIVE
|
||||||
try {
|
qsTile.updateTile()
|
||||||
when (it) {
|
}
|
||||||
Tunnel.State.UP -> {
|
|
||||||
qsTile.state = Tile.STATE_ACTIVE
|
|
||||||
}
|
|
||||||
|
|
||||||
Tunnel.State.DOWN -> {
|
private fun setInactive() {
|
||||||
qsTile.state = Tile.STATE_INACTIVE
|
qsTile.state = Tile.STATE_INACTIVE
|
||||||
}
|
qsTile.updateTile()
|
||||||
|
}
|
||||||
|
|
||||||
else -> {
|
private fun setUnavailable() {
|
||||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||||
}
|
qsTile.updateTile()
|
||||||
}
|
|
||||||
val config = determineTileTunnel()
|
|
||||||
setTileDescription(
|
|
||||||
config?.name ?: this.resources.getString(R.string.no_tunnel_available)
|
|
||||||
)
|
|
||||||
qsTile.updateTile()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e("Unable to update tile state")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setTileDescription(description: String) {
|
private fun setTileDescription(description: String) {
|
||||||
|
@ -149,11 +126,6 @@ class TunnelControlTile : TileService() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
qsTile.stateDescription = description
|
qsTile.stateDescription = description
|
||||||
}
|
}
|
||||||
}
|
qsTile.updateTile()
|
||||||
|
|
||||||
private fun cancelJob() {
|
|
||||||
if (this::job.isInitialized) {
|
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,7 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||||
enum class HandshakeStatus {
|
enum class HandshakeStatus {
|
||||||
HEALTHY,
|
HEALTHY,
|
||||||
STALE,
|
STALE,
|
||||||
UNHEALTHY,
|
UNKNOWN,
|
||||||
NEVER_CONNECTED,
|
|
||||||
NOT_STARTED
|
NOT_STARTED
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,15 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||||
|
|
||||||
import com.wireguard.android.backend.Statistics
|
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.wireguard.crypto.Key
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
|
||||||
|
|
||||||
interface VpnService : Tunnel {
|
interface VpnService : Tunnel {
|
||||||
suspend fun startTunnel(tunnelConfig: TunnelConfig): Tunnel.State
|
suspend fun startTunnel(tunnelConfig: TunnelConfig): Tunnel.State
|
||||||
|
|
||||||
suspend fun stopTunnel()
|
suspend fun stopTunnel()
|
||||||
|
|
||||||
val state: SharedFlow<Tunnel.State>
|
val vpnState: StateFlow<VpnState>
|
||||||
val tunnelName: SharedFlow<String>
|
|
||||||
val statistics: SharedFlow<Statistics>
|
|
||||||
val lastHandshake: SharedFlow<Map<Key, Long>>
|
|
||||||
val handshakeStatus: SharedFlow<HandshakeStatus>
|
|
||||||
|
|
||||||
fun getState(): Tunnel.State
|
fun getState(): Tunnel.State
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||||
|
|
||||||
|
import com.wireguard.android.backend.Statistics
|
||||||
|
import com.wireguard.android.backend.Tunnel
|
||||||
|
|
||||||
|
data class VpnState(
|
||||||
|
val status : Tunnel.State = Tunnel.State.DOWN,
|
||||||
|
val name : String = "",
|
||||||
|
val statistics : Statistics? = null
|
||||||
|
)
|
|
@ -3,60 +3,34 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||||
import com.wireguard.android.backend.Backend
|
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
|
import com.wireguard.android.backend.Tunnel.State
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
import com.wireguard.crypto.Key
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
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.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class WireGuardTunnel
|
class WireGuardTunnel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
@Userspace private val userspaceBackend: Backend,
|
@Userspace private val userspaceBackend: Backend,
|
||||||
@Kernel private val kernelBackend: Backend,
|
@Kernel private val kernelBackend: Backend,
|
||||||
private val settingsRepo: SettingsDoa
|
private val settingsRepository: SettingsRepository
|
||||||
) : VpnService {
|
) : VpnService {
|
||||||
private val _tunnelName = MutableStateFlow("")
|
private val _vpnState = MutableStateFlow(VpnState())
|
||||||
override val tunnelName get() = _tunnelName.asStateFlow()
|
override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow()
|
||||||
|
|
||||||
private val _state =
|
|
||||||
MutableSharedFlow<Tunnel.State>(
|
|
||||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
|
||||||
replay = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
private val _handshakeStatus =
|
|
||||||
MutableSharedFlow<HandshakeStatus>(
|
|
||||||
replay = 1,
|
|
||||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
|
||||||
)
|
|
||||||
override val state get() = _state.asSharedFlow()
|
|
||||||
|
|
||||||
private val _statistics = MutableSharedFlow<Statistics>(replay = 1)
|
|
||||||
override val statistics get() = _statistics.asSharedFlow()
|
|
||||||
|
|
||||||
private val _lastHandshake = MutableSharedFlow<Map<Key, Long>>(replay = 1)
|
|
||||||
override val lastHandshake get() = _lastHandshake.asSharedFlow()
|
|
||||||
|
|
||||||
override val handshakeStatus: SharedFlow<HandshakeStatus>
|
|
||||||
get() = _handshakeStatus.asSharedFlow()
|
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO)
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
|
@ -70,13 +44,12 @@ constructor(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
settingsRepo.getAllFlow().collect {
|
settingsRepository.getSettingsFlow().collect {
|
||||||
val settings = it.first()
|
if (it.isKernelEnabled && backendIsUserspace) {
|
||||||
if (settings.isKernelEnabled && backendIsUserspace) {
|
|
||||||
Timber.d("Setting kernel backend")
|
Timber.d("Setting kernel backend")
|
||||||
backend = kernelBackend
|
backend = kernelBackend
|
||||||
backendIsUserspace = false
|
backendIsUserspace = false
|
||||||
} else if (!settings.isKernelEnabled && !backendIsUserspace) {
|
} else if (!it.isKernelEnabled && !backendIsUserspace) {
|
||||||
Timber.d("Setting userspace backend")
|
Timber.d("Setting userspace backend")
|
||||||
backend = userspaceBackend
|
backend = userspaceBackend
|
||||||
backendIsUserspace = true
|
backendIsUserspace = true
|
||||||
|
@ -85,7 +58,7 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig): Tunnel.State {
|
override suspend fun startTunnel(tunnelConfig: TunnelConfig): State {
|
||||||
return try {
|
return try {
|
||||||
stopTunnelOnConfigChange(tunnelConfig)
|
stopTunnelOnConfigChange(tunnelConfig)
|
||||||
emitTunnelName(tunnelConfig.name)
|
emitTunnelName(tunnelConfig.name)
|
||||||
|
@ -93,95 +66,84 @@ constructor(
|
||||||
val state =
|
val state =
|
||||||
backend.setState(
|
backend.setState(
|
||||||
this,
|
this,
|
||||||
Tunnel.State.UP,
|
State.UP,
|
||||||
config
|
config
|
||||||
)
|
)
|
||||||
_state.emit(state)
|
emitTunnelState(state)
|
||||||
state
|
state
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e("Failed to start tunnel with error: ${e.message}")
|
Timber.e("Failed to start tunnel with error: ${e.message}")
|
||||||
Tunnel.State.DOWN
|
State.DOWN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun emitTunnelState(state: State) {
|
||||||
|
_vpnState.tryEmit(
|
||||||
|
_vpnState.value.copy(
|
||||||
|
status = state
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emitBackendStatistics(statistics: Statistics) {
|
||||||
|
_vpnState.tryEmit(
|
||||||
|
_vpnState.value.copy(
|
||||||
|
statistics = statistics
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun emitTunnelName(name: String) {
|
private suspend fun emitTunnelName(name: String) {
|
||||||
_tunnelName.emit(name)
|
_vpnState.emit(
|
||||||
|
_vpnState.value.copy(
|
||||||
|
name = name
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun stopTunnelOnConfigChange(tunnelConfig: TunnelConfig) {
|
private suspend fun stopTunnelOnConfigChange(tunnelConfig: TunnelConfig) {
|
||||||
if (getState() == Tunnel.State.UP && _tunnelName.value != tunnelConfig.name) {
|
if (getState() == State.UP && _vpnState.value.name != tunnelConfig.name) {
|
||||||
stopTunnel()
|
stopTunnel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getName(): String {
|
override fun getName(): String {
|
||||||
return _tunnelName.value
|
return _vpnState.value.name
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun stopTunnel() {
|
override suspend fun stopTunnel() {
|
||||||
try {
|
try {
|
||||||
if (getState() == Tunnel.State.UP) {
|
if (getState() == State.UP) {
|
||||||
val state = backend.setState(this, Tunnel.State.DOWN, null)
|
val state = backend.setState(this, State.DOWN, null)
|
||||||
_state.emit(state)
|
emitTunnelState(state)
|
||||||
}
|
}
|
||||||
} catch (e: BackendException) {
|
} catch (e: BackendException) {
|
||||||
Timber.e("Failed to stop tunnel with error: ${e.message}")
|
Timber.e("Failed to stop tunnel with error: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getState(): Tunnel.State {
|
override fun getState(): State {
|
||||||
return backend.getState(this)
|
return backend.getState(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStateChange(state: Tunnel.State) {
|
override fun onStateChange(state: State) {
|
||||||
val tunnel = this
|
val tunnel = this
|
||||||
_state.tryEmit(state)
|
emitTunnelState(state)
|
||||||
if (state == Tunnel.State.UP) {
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
|
if (state == State.UP) {
|
||||||
statsJob =
|
statsJob =
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val handshakeMap = HashMap<Key, Long>()
|
|
||||||
var neverHadHandshakeCounter = 0
|
|
||||||
while (true) {
|
while (true) {
|
||||||
val statistics = backend.getStatistics(tunnel)
|
val statistics = backend.getStatistics(tunnel)
|
||||||
_statistics.emit(statistics)
|
emitBackendStatistics(statistics)
|
||||||
statistics.peers().forEach { key ->
|
|
||||||
val handshakeEpoch =
|
|
||||||
statistics.peer(key)?.latestHandshakeEpochMillis ?: 0L
|
|
||||||
handshakeMap[key] = handshakeEpoch
|
|
||||||
if (handshakeEpoch == 0L) {
|
|
||||||
if (neverHadHandshakeCounter >= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) {
|
|
||||||
_handshakeStatus.emit(HandshakeStatus.NEVER_CONNECTED)
|
|
||||||
} else {
|
|
||||||
_handshakeStatus.emit(HandshakeStatus.NOT_STARTED)
|
|
||||||
}
|
|
||||||
if (neverHadHandshakeCounter <= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) {
|
|
||||||
neverHadHandshakeCounter += (1 * Constants.VPN_STATISTIC_CHECK_INTERVAL / 1000).toInt()
|
|
||||||
}
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
// TODO one day make each peer have their own dedicated status
|
|
||||||
val lastHandshake = NumberUtils.getSecondsBetweenTimestampAndNow(
|
|
||||||
handshakeEpoch
|
|
||||||
)
|
|
||||||
if (lastHandshake != null) {
|
|
||||||
if (lastHandshake >= HandshakeStatus.STALE_TIME_LIMIT_SEC) {
|
|
||||||
_handshakeStatus.emit(HandshakeStatus.STALE)
|
|
||||||
} else {
|
|
||||||
_handshakeStatus.emit(HandshakeStatus.HEALTHY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_lastHandshake.emit(handshakeMap)
|
|
||||||
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
|
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (state == Tunnel.State.DOWN) {
|
if (state == State.DOWN) {
|
||||||
if (this::statsJob.isInitialized) {
|
if (this::statsJob.isInitialized) {
|
||||||
statsJob.cancel()
|
statsJob.cancel()
|
||||||
}
|
}
|
||||||
_handshakeStatus.tryEmit(HandshakeStatus.NOT_STARTED)
|
|
||||||
_lastHandshake.tryEmit(emptyMap())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.ui
|
package com.zaneschepke.wireguardautotunnel.ui
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ActivityViewModel @Inject constructor() : ViewModel() {
|
@HiltViewModel
|
||||||
// TODO move shared logic to shared viewmodel
|
class ActivityViewModel @Inject constructor(
|
||||||
|
private val settingsRepo: SettingsDao,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,15 +6,11 @@ import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.view.KeyEvent
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.animation.ExitTransition
|
import androidx.compose.foundation.focusable
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.slideInHorizontally
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarData
|
import androidx.compose.material3.SnackbarData
|
||||||
|
@ -30,7 +26,7 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
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.input.key.onKeyEvent
|
import androidx.compose.ui.focus.focusProperties
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
|
@ -40,7 +36,6 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.isGranted
|
import com.google.accompanist.permissions.isGranted
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
import com.wireguard.android.backend.GoBackend
|
import com.wireguard.android.backend.GoBackend
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
|
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
||||||
|
@ -51,10 +46,10 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars
|
import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
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
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
@ -64,10 +59,10 @@ class MainActivity : AppCompatActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContent {
|
setContent {
|
||||||
// TODO move shared logic to shared viewmodel
|
// val activityViewModel = hiltViewModel<ActivityViewModel>()
|
||||||
// val sharedViewModel = hiltViewModel<ActivityViewModel>()
|
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester()}
|
||||||
|
|
||||||
WireguardAutoTunnelTheme {
|
WireguardAutoTunnelTheme {
|
||||||
TransparentSystemBars()
|
TransparentSystemBars()
|
||||||
|
@ -104,18 +99,13 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
fun showSnackBarMessage(message: String) {
|
fun showSnackBarMessage(message: String) {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
val result =
|
val result = snackbarHostState.showSnackbar(
|
||||||
snackbarHostState.showSnackbar(
|
|
||||||
message = message,
|
message = message,
|
||||||
actionLabel = applicationContext.getString(R.string.okay),
|
actionLabel = applicationContext.getString(R.string.okay),
|
||||||
duration = SnackbarDuration.Short
|
duration = SnackbarDuration.Short
|
||||||
)
|
)
|
||||||
when (result) {
|
when (result) {
|
||||||
SnackbarResult.ActionPerformed -> {
|
SnackbarResult.ActionPerformed, SnackbarResult.Dismissed -> {
|
||||||
snackbarHostState.currentSnackbarData?.dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
SnackbarResult.Dismissed -> {
|
|
||||||
snackbarHostState.currentSnackbarData?.dismiss()
|
snackbarHostState.currentSnackbarData?.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -134,32 +124,13 @@ class MainActivity : AppCompatActivity() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier =
|
modifier = Modifier.focusable().focusProperties { up = focusRequester },
|
||||||
Modifier.onKeyEvent {
|
|
||||||
if (it.nativeKeyEvent.action == KeyEvent.ACTION_UP) {
|
|
||||||
when (it.nativeKeyEvent.keyCode) {
|
|
||||||
KeyEvent.KEYCODE_DPAD_UP -> {
|
|
||||||
try {
|
|
||||||
focusRequester.requestFocus()
|
|
||||||
} catch (e: IllegalStateException) {
|
|
||||||
Timber.e(
|
|
||||||
"No D-Pad focus request modifier added to element on screen"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
bottomBar =
|
bottomBar =
|
||||||
if (vpnIntent == null && notificationPermissionState.status.isGranted) {
|
if (vpnIntent == null && notificationPermissionState.status.isGranted) {
|
||||||
{ BottomNavBar(navController, Routes.navItems) }
|
{ BottomNavBar(navController, listOf(
|
||||||
|
Screen.Main.navItem,
|
||||||
|
Screen.Settings.navItem,
|
||||||
|
Screen.Support.navItem)) }
|
||||||
} else {
|
} else {
|
||||||
{}
|
{}
|
||||||
}
|
}
|
||||||
|
@ -192,85 +163,31 @@ class MainActivity : AppCompatActivity() {
|
||||||
)
|
)
|
||||||
return@Scaffold
|
return@Scaffold
|
||||||
}
|
}
|
||||||
|
NavHost(navController, startDestination = Screen.Main.route) {
|
||||||
NavHost(navController, startDestination = Routes.Main.name) {
|
|
||||||
composable(
|
composable(
|
||||||
Routes.Main.name,
|
Screen.Main.route,
|
||||||
enterTransition = {
|
|
||||||
when (initialState.destination.route) {
|
|
||||||
Routes.Settings.name, Routes.Support.name ->
|
|
||||||
slideInHorizontally(
|
|
||||||
initialOffsetX = {
|
|
||||||
-Constants.SLIDE_IN_TRANSITION_OFFSET
|
|
||||||
},
|
|
||||||
animationSpec = tween(
|
|
||||||
Constants.SLIDE_IN_ANIMATION_DURATION
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
fadeIn(
|
|
||||||
animationSpec = tween(
|
|
||||||
Constants.FADE_IN_ANIMATION_DURATION
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
exitTransition = {
|
|
||||||
ExitTransition.None
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
MainScreen(padding = padding, showSnackbarMessage = { message ->
|
MainScreen(padding = padding, focusRequester = focusRequester, showSnackbarMessage = { message ->
|
||||||
showSnackBarMessage(message)
|
showSnackBarMessage(message)
|
||||||
}, navController = navController)
|
}, navController = navController)
|
||||||
}
|
}
|
||||||
composable(Routes.Settings.name, enterTransition = {
|
composable(Screen.Settings.route,
|
||||||
when (initialState.destination.route) {
|
) {
|
||||||
Routes.Main.name ->
|
|
||||||
slideInHorizontally(
|
|
||||||
initialOffsetX = { Constants.SLIDE_IN_TRANSITION_OFFSET },
|
|
||||||
animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION)
|
|
||||||
)
|
|
||||||
|
|
||||||
Routes.Support.name -> {
|
|
||||||
slideInHorizontally(
|
|
||||||
initialOffsetX = { -Constants.SLIDE_IN_TRANSITION_OFFSET },
|
|
||||||
animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
fadeIn(
|
|
||||||
animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
SettingsScreen(padding = padding, showSnackbarMessage = { message ->
|
SettingsScreen(padding = padding, showSnackbarMessage = { message ->
|
||||||
showSnackBarMessage(message)
|
showSnackBarMessage(message)
|
||||||
}, focusRequester = focusRequester)
|
}, focusRequester = focusRequester)
|
||||||
}
|
}
|
||||||
composable(Routes.Support.name, enterTransition = {
|
composable(Screen.Support.route,
|
||||||
when (initialState.destination.route) {
|
) {
|
||||||
Routes.Settings.name, Routes.Main.name ->
|
SupportScreen(padding = padding, focusRequester = focusRequester,
|
||||||
slideInHorizontally(
|
showSnackbarMessage = { message ->
|
||||||
initialOffsetX = { Constants.SLIDE_IN_ANIMATION_DURATION },
|
showSnackBarMessage(message)
|
||||||
animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION)
|
})
|
||||||
)
|
}
|
||||||
|
composable("${Screen.Config.route}/{id}") {
|
||||||
else -> {
|
|
||||||
fadeIn(
|
|
||||||
animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) { SupportScreen(padding = padding, focusRequester = focusRequester) }
|
|
||||||
composable("${Routes.Config.name}/{id}", enterTransition = {
|
|
||||||
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
|
|
||||||
}) {
|
|
||||||
val id = it.arguments?.getString("id")
|
val id = it.arguments?.getString("id")
|
||||||
if (!id.isNullOrBlank()) {
|
if (!id.isNullOrBlank()) {
|
||||||
|
//https://dagger.dev/hilt/view-model#assisted-injection
|
||||||
ConfigScreen(
|
ConfigScreen(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
id = id,
|
id = id,
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.ui
|
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.rounded.Home
|
|
||||||
import androidx.compose.material.icons.rounded.QuestionMark
|
|
||||||
import androidx.compose.material.icons.rounded.Settings
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
|
|
||||||
|
|
||||||
enum class Routes {
|
|
||||||
Main,
|
|
||||||
Settings,
|
|
||||||
Support,
|
|
||||||
Config
|
|
||||||
;
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val navItems =
|
|
||||||
listOf(
|
|
||||||
BottomNavItem(
|
|
||||||
name = "Tunnels",
|
|
||||||
route = Main.name,
|
|
||||||
icon = Icons.Rounded.Home
|
|
||||||
),
|
|
||||||
BottomNavItem(
|
|
||||||
name = "Settings",
|
|
||||||
route = Settings.name,
|
|
||||||
icon = Icons.Rounded.Settings
|
|
||||||
),
|
|
||||||
BottomNavItem(
|
|
||||||
name = "Support",
|
|
||||||
route = Support.name,
|
|
||||||
icon = Icons.Rounded.QuestionMark
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.Home
|
||||||
|
import androidx.compose.material.icons.rounded.QuestionMark
|
||||||
|
import androidx.compose.material.icons.rounded.Settings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
|
||||||
|
|
||||||
|
sealed class Screen(val route : String) {
|
||||||
|
data object Main: Screen("main") {
|
||||||
|
val navItem = BottomNavItem(
|
||||||
|
name = "Tunnels",
|
||||||
|
route = route,
|
||||||
|
icon = Icons.Rounded.Home
|
||||||
|
)
|
||||||
|
}
|
||||||
|
data object Settings: Screen("settings") {
|
||||||
|
val navItem = BottomNavItem(
|
||||||
|
name = "Settings",
|
||||||
|
route = route,
|
||||||
|
icon = Icons.Rounded.Settings
|
||||||
|
)
|
||||||
|
}
|
||||||
|
data object Support: Screen("support") {
|
||||||
|
val navItem = BottomNavItem(
|
||||||
|
name = "Support",
|
||||||
|
route = route,
|
||||||
|
icon = Icons.Rounded.QuestionMark
|
||||||
|
)
|
||||||
|
}
|
||||||
|
data object Config : Screen("config")
|
||||||
|
|
||||||
|
}
|
|
@ -1,10 +1,8 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.ui.common
|
package com.zaneschepke.wireguardautotunnel.ui.common
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
@ -13,18 +11,18 @@ 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 androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ClickableIconButton(
|
fun ClickableIconButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
onIconClick: () -> Unit,
|
onIconClick: () -> Unit,
|
||||||
text: String,
|
text: String,
|
||||||
icon: ImageVector,
|
icon: ImageVector,
|
||||||
enabled: Boolean
|
enabled: Boolean
|
||||||
) {
|
) {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {},
|
onClick = onClick,
|
||||||
enabled = enabled
|
enabled = enabled
|
||||||
) {
|
) {
|
||||||
Text(text, Modifier.weight(1f, false))
|
Text(text, Modifier.weight(1f, false))
|
||||||
|
|
|
@ -18,8 +18,8 @@ import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.wireguard.android.backend.Statistics
|
import com.wireguard.android.backend.Statistics
|
||||||
import com.zaneschepke.wireguardautotunnel.toThreeDecimalPlaceString
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.toThreeDecimalPlaceString
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -51,7 +51,7 @@ fun RowListItem(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 14.dp, vertical = 5.dp),
|
.padding(horizontal = 15.dp, vertical = 5.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -38,7 +38,7 @@ fun CustomSnackBar(
|
||||||
containerColor = containerColor,
|
containerColor = containerColor,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth(
|
Modifier.fillMaxWidth(
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) 1 / 3f else 2 / 3f
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f
|
||||||
).padding(bottom = 100.dp),
|
).padding(bottom = 100.dp),
|
||||||
shape = RoundedCornerShape(16.dp)
|
shape = RoundedCornerShape(16.dp)
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common.screen
|
||||||
|
|
||||||
|
import androidx.compose.foundation.focusable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoadingScreen() {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier = Modifier.fillMaxSize().focusable().padding()) {
|
||||||
|
Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() }
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,18 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.config
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Packages
|
||||||
|
|
||||||
|
data class ConfigUiState(
|
||||||
|
val proxyPeers: List<PeerProxy> = arrayListOf(PeerProxy()),
|
||||||
|
val interfaceProxy: InterfaceProxy = InterfaceProxy(),
|
||||||
|
val packages: Packages = emptyList(),
|
||||||
|
val checkedPackageNames: List<String> = emptyList(),
|
||||||
|
val include: Boolean = true,
|
||||||
|
val isAllApplicationsEnabled : Boolean = false,
|
||||||
|
val loading: Boolean = true,
|
||||||
|
val tunnel: TunnelConfig? = null,
|
||||||
|
val tunnelName: String = ""
|
||||||
|
)
|
|
@ -5,8 +5,6 @@ import android.app.Application
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
|
||||||
import androidx.compose.runtime.toMutableStateList
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
|
@ -14,426 +12,301 @@ import com.wireguard.config.Interface
|
||||||
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.Constants
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
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.Event
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.removeAt
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.update
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
|
||||||
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 javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ConfigViewModel
|
class ConfigViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
private val tunnelRepo: TunnelConfigDao,
|
private val tunnelConfigRepository: TunnelConfigRepository,
|
||||||
private val settingsRepo: SettingsDoa
|
private val settingsRepository: SettingsRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _tunnel = MutableStateFlow<TunnelConfig?>(null)
|
|
||||||
private val _tunnelName = MutableStateFlow("")
|
|
||||||
val tunnelName get() = _tunnelName.asStateFlow()
|
|
||||||
val tunnel get() = _tunnel.asStateFlow()
|
|
||||||
|
|
||||||
private var _proxyPeers = MutableStateFlow(mutableStateListOf<PeerProxy>())
|
private val packageManager = application.packageManager
|
||||||
val proxyPeers get() = _proxyPeers.asStateFlow()
|
|
||||||
|
|
||||||
private var _interface = MutableStateFlow(InterfaceProxy())
|
private val _uiState = MutableStateFlow(ConfigUiState())
|
||||||
val interfaceProxy = _interface.asStateFlow()
|
val uiState = _uiState.asStateFlow()
|
||||||
|
|
||||||
private val _packages = MutableStateFlow(emptyList<PackageInfo>())
|
fun init(tunnelId : String) = viewModelScope.launch(Dispatchers.IO) {
|
||||||
val packages get() = _packages.asStateFlow()
|
val packages = getQueriedPackages("")
|
||||||
private val packageManager = application.packageManager
|
val state = if(tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
||||||
|
|
||||||
private val _checkedPackages = MutableStateFlow(mutableStateListOf<String>())
|
|
||||||
val checkedPackages get() = _checkedPackages.asStateFlow()
|
|
||||||
private val _include = MutableStateFlow(true)
|
|
||||||
val include get() = _include.asStateFlow()
|
|
||||||
|
|
||||||
private val _isAllApplicationsEnabled = MutableStateFlow(false)
|
|
||||||
val isAllApplicationsEnabled get() = _isAllApplicationsEnabled.asStateFlow()
|
|
||||||
private val _isDefaultTunnel = MutableStateFlow(false)
|
|
||||||
|
|
||||||
private lateinit var tunnelConfig: TunnelConfig
|
|
||||||
|
|
||||||
suspend fun onScreenLoad(id: String) {
|
|
||||||
if (id != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
|
||||||
tunnelConfig = getTunnelConfigById(id) ?: throw WgTunnelException(
|
|
||||||
"Config not found"
|
|
||||||
)
|
|
||||||
emitScreenData()
|
|
||||||
} else {
|
|
||||||
emitEmptyScreenData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun emitEmptyScreenData() {
|
|
||||||
tunnelConfig = TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = "")
|
|
||||||
viewModelScope.launch {
|
|
||||||
emitTunnelConfig()
|
|
||||||
emitPeerProxy(PeerProxy())
|
|
||||||
emitInterfaceProxy(InterfaceProxy())
|
|
||||||
emitTunnelConfigName()
|
|
||||||
emitDefaultTunnelStatus()
|
|
||||||
emitQueriedPackages("")
|
|
||||||
emitTunnelAllApplicationsEnabled()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitScreenData() {
|
|
||||||
emitTunnelConfig()
|
|
||||||
emitPeersFromConfig()
|
|
||||||
emitInterfaceFromConfig()
|
|
||||||
emitTunnelConfigName()
|
|
||||||
emitDefaultTunnelStatus()
|
|
||||||
emitQueriedPackages("")
|
|
||||||
emitCurrentPackageConfigurations()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitDefaultTunnelStatus() {
|
|
||||||
val settings = settingsRepo.getAll()
|
|
||||||
if (settings.isNotEmpty()) {
|
|
||||||
_isDefaultTunnel.value = settings.first().isTunnelConfigDefault(tunnelConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun emitInterfaceFromConfig() {
|
|
||||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
|
||||||
_interface.value = InterfaceProxy.from(config.`interface`)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun emitPeersFromConfig() {
|
|
||||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
|
||||||
config.peers.forEach {
|
|
||||||
_proxyPeers.value.add(PeerProxy.from(it))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun emitPeerProxy(peerProxy: PeerProxy) {
|
|
||||||
_proxyPeers.value.add(peerProxy)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun emitInterfaceProxy(interfaceProxy: InterfaceProxy) {
|
|
||||||
_interface.value = interfaceProxy
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getTunnelConfigById(id: String): TunnelConfig? {
|
|
||||||
return try {
|
|
||||||
tunnelRepo.getById(id.toLong())
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitTunnelConfig() {
|
|
||||||
_tunnel.emit(tunnelConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitTunnelConfigName() {
|
|
||||||
_tunnelName.emit(tunnelConfig.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onTunnelNameChange(name: String) {
|
|
||||||
_tunnelName.value = name
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onIncludeChange(include: Boolean) {
|
|
||||||
_include.value = include
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onAddCheckedPackage(packageName: String) {
|
|
||||||
_checkedPackages.value.add(packageName)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onAllApplicationsChange(isAllApplicationsEnabled: Boolean) {
|
|
||||||
_isAllApplicationsEnabled.value = isAllApplicationsEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onRemoveCheckedPackage(packageName: String) {
|
|
||||||
_checkedPackages.value.remove(packageName)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitSplitTunnelConfiguration(config: Config) {
|
|
||||||
val excludedApps = config.`interface`.excludedApplications
|
|
||||||
val includedApps = config.`interface`.includedApplications
|
|
||||||
if (excludedApps.isNotEmpty() || includedApps.isNotEmpty()) {
|
|
||||||
emitTunnelAllApplicationsDisabled()
|
|
||||||
determineAppInclusionState(excludedApps, includedApps)
|
|
||||||
} else {
|
|
||||||
emitTunnelAllApplicationsEnabled()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun determineAppInclusionState(
|
|
||||||
excludedApps: Set<String>,
|
|
||||||
includedApps: Set<String>
|
|
||||||
) {
|
|
||||||
if (excludedApps.isEmpty()) {
|
|
||||||
emitIncludedAppsExist()
|
|
||||||
emitCheckedApps(includedApps)
|
|
||||||
} else {
|
|
||||||
emitExcludedAppsExist()
|
|
||||||
emitCheckedApps(excludedApps)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitIncludedAppsExist() {
|
|
||||||
_include.emit(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitExcludedAppsExist() {
|
|
||||||
_include.emit(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitCheckedApps(apps: Set<String>) {
|
|
||||||
_checkedPackages.emit(apps.toMutableStateList())
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitTunnelAllApplicationsEnabled() {
|
|
||||||
_isAllApplicationsEnabled.emit(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitTunnelAllApplicationsDisabled() {
|
|
||||||
_isAllApplicationsEnabled.emit(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun emitCurrentPackageConfigurations() {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
|
||||||
emitSplitTunnelConfiguration(config)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun emitQueriedPackages(query: String) {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
val packages =
|
|
||||||
getAllInternetCapablePackages().filter {
|
|
||||||
getPackageLabel(it).lowercase().contains(query.lowercase())
|
|
||||||
}
|
|
||||||
_packages.emit(packages)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPackageLabel(packageInfo: PackageInfo): String {
|
|
||||||
return packageInfo.applicationInfo.loadLabel(application.packageManager).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getAllInternetCapablePackages(): List<PackageInfo> {
|
|
||||||
return getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getPackagesHoldingPermissions(permissions: Array<String>): List<PackageInfo> {
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
packageManager.getPackagesHoldingPermissions(
|
|
||||||
permissions,
|
|
||||||
PackageManager.PackageInfoFlags.of(0L)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
packageManager.getPackagesHoldingPermissions(permissions, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isAllApplicationsEnabled(): Boolean {
|
|
||||||
return _isAllApplicationsEnabled.value
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun saveConfig(tunnelConfig: TunnelConfig) {
|
|
||||||
tunnelRepo.save(tunnelConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun updateTunnelConfig(tunnelConfig: TunnelConfig?) {
|
|
||||||
if (tunnelConfig != null) {
|
|
||||||
saveConfig(tunnelConfig)
|
|
||||||
updateSettingsDefaultTunnel(tunnelConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun updateSettingsDefaultTunnel(tunnelConfig: TunnelConfig) {
|
|
||||||
val settings = settingsRepo.getAll()
|
|
||||||
if (settings.isNotEmpty()) {
|
|
||||||
val setting = settings[0]
|
|
||||||
if (setting.defaultTunnel != null) {
|
|
||||||
if (tunnelConfig.id == TunnelConfig.from(setting.defaultTunnel!!).id) {
|
|
||||||
settingsRepo.save(
|
|
||||||
setting.copy(
|
|
||||||
defaultTunnel = tunnelConfig.toString()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildPeerListFromProxyPeers(): List<Peer> {
|
|
||||||
return _proxyPeers.value.map {
|
|
||||||
val builder = Peer.Builder()
|
|
||||||
if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim())
|
|
||||||
if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim())
|
|
||||||
if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.trim())
|
|
||||||
if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.trim())
|
|
||||||
if (it.persistentKeepalive.isNotEmpty()) {
|
|
||||||
builder.parsePersistentKeepalive(
|
|
||||||
it.persistentKeepalive.trim()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
builder.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildInterfaceListFromProxyInterface(): Interface {
|
|
||||||
val builder = Interface.Builder()
|
|
||||||
builder.parsePrivateKey(_interface.value.privateKey.trim())
|
|
||||||
builder.parseAddresses(_interface.value.addresses.trim())
|
|
||||||
builder.parseDnsServers(_interface.value.dnsServers.trim())
|
|
||||||
if (_interface.value.mtu.isNotEmpty()) builder.parseMtu(_interface.value.mtu.trim())
|
|
||||||
if (_interface.value.listenPort.isNotEmpty()) {
|
|
||||||
builder.parseListenPort(
|
|
||||||
_interface.value.listenPort.trim()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (isAllApplicationsEnabled()) _checkedPackages.value.clear()
|
|
||||||
if (_include.value) builder.includeApplications(_checkedPackages.value)
|
|
||||||
if (!_include.value) builder.excludeApplications(_checkedPackages.value)
|
|
||||||
return builder.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun onSaveAllChanges() {
|
|
||||||
try {
|
|
||||||
val peerList = buildPeerListFromProxyPeers()
|
|
||||||
val wgInterface = buildInterfaceListFromProxyInterface()
|
|
||||||
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
|
||||||
val tunnelConfig =
|
val tunnelConfig =
|
||||||
_tunnel.value?.copy(
|
tunnelConfigRepository.getAll().firstOrNull { it.id.toString() == tunnelId }
|
||||||
name = _tunnelName.value,
|
if (tunnelConfig != null) {
|
||||||
wgQuick = config.toWgQuickString()
|
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||||
)
|
val proxyPeers = config.peers.map { PeerProxy.from(it) }
|
||||||
updateTunnelConfig(tunnelConfig)
|
val proxyInterface = InterfaceProxy.from(config.`interface`)
|
||||||
} catch (e: Exception) {
|
var include = true
|
||||||
throw WgTunnelException(
|
var isAllApplicationsEnabled = false
|
||||||
"Error: ${e.cause?.message?.lowercase() ?: "unknown error occurred"}"
|
val checkedPackages =
|
||||||
)
|
if (config.`interface`.includedApplications.isNotEmpty()) {
|
||||||
}
|
config.`interface`.includedApplications
|
||||||
}
|
} else if (config.`interface`.excludedApplications.isNotEmpty()) {
|
||||||
|
include = false
|
||||||
fun onPeerPublicKeyChange(
|
config.`interface`.excludedApplications
|
||||||
index: Int,
|
} else {
|
||||||
publicKey: String
|
isAllApplicationsEnabled = true
|
||||||
) {
|
emptySet()
|
||||||
_proxyPeers.value[index] =
|
}
|
||||||
_proxyPeers.value[index].copy(
|
ConfigUiState(
|
||||||
publicKey = publicKey
|
proxyPeers,
|
||||||
)
|
proxyInterface,
|
||||||
}
|
packages,
|
||||||
|
checkedPackages.toList(),
|
||||||
fun onPreSharedKeyChange(
|
include,
|
||||||
index: Int,
|
isAllApplicationsEnabled,
|
||||||
value: String
|
false,
|
||||||
) {
|
tunnelConfig,
|
||||||
_proxyPeers.value[index] =
|
tunnelConfig.name)
|
||||||
_proxyPeers.value[index].copy(
|
} else {
|
||||||
preSharedKey = value
|
ConfigUiState(loading = false, packages = packages)
|
||||||
)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun onEndpointChange(
|
|
||||||
index: Int,
|
|
||||||
value: String
|
|
||||||
) {
|
|
||||||
_proxyPeers.value[index] =
|
|
||||||
_proxyPeers.value[index].copy(
|
|
||||||
endpoint = value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onAllowedIpsChange(
|
|
||||||
index: Int,
|
|
||||||
value: String
|
|
||||||
) {
|
|
||||||
_proxyPeers.value[index] =
|
|
||||||
_proxyPeers.value[index].copy(
|
|
||||||
allowedIps = value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onPersistentKeepaliveChanged(
|
|
||||||
index: Int,
|
|
||||||
value: String
|
|
||||||
) {
|
|
||||||
_proxyPeers.value[index] =
|
|
||||||
_proxyPeers.value[index].copy(
|
|
||||||
persistentKeepalive = value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onDeletePeer(index: Int) {
|
|
||||||
proxyPeers.value.removeAt(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addEmptyPeer() {
|
|
||||||
_proxyPeers.value.add(PeerProxy())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generateKeyPair() {
|
|
||||||
val keyPair = KeyPair()
|
|
||||||
_interface.value =
|
|
||||||
_interface.value.copy(
|
|
||||||
privateKey = keyPair.privateKey.toBase64(),
|
|
||||||
publicKey = keyPair.publicKey.toBase64()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onAddressesChanged(value: String) {
|
|
||||||
_interface.value =
|
|
||||||
_interface.value.copy(
|
|
||||||
addresses = value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onListenPortChanged(value: String) {
|
|
||||||
_interface.value =
|
|
||||||
_interface.value.copy(
|
|
||||||
listenPort = value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onDnsServersChanged(value: String) {
|
|
||||||
_interface.value =
|
|
||||||
_interface.value.copy(
|
|
||||||
dnsServers = value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onMtuChanged(value: String) {
|
|
||||||
_interface.value =
|
|
||||||
_interface.value.copy(
|
|
||||||
mtu = value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onInterfacePublicKeyChange(value: String) {
|
|
||||||
_interface.value =
|
|
||||||
_interface.value.copy(
|
|
||||||
publicKey = value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onPrivateKeyChange(value: String) {
|
|
||||||
_interface.value =
|
|
||||||
_interface.value.copy(
|
|
||||||
privateKey = value
|
|
||||||
)
|
|
||||||
if (NumberUtils.isValidKey(value)) {
|
|
||||||
val pair = KeyPair(Key.fromBase64(value))
|
|
||||||
onInterfacePublicKeyChange(pair.publicKey.toBase64())
|
|
||||||
} else {
|
} else {
|
||||||
onInterfacePublicKeyChange("")
|
ConfigUiState(loading = false, packages = packages)
|
||||||
}
|
}
|
||||||
|
_uiState.value = state
|
||||||
}
|
}
|
||||||
|
fun onTunnelNameChange(name: String) {
|
||||||
|
_uiState.value = _uiState.value.copy(tunnelName = name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onIncludeChange(include: Boolean) {
|
||||||
|
_uiState.value = _uiState.value.copy(include = include)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAddCheckedPackage(packageName: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(checkedPackageNames = _uiState.value.checkedPackageNames + packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAllApplicationsChange(isAllApplicationsEnabled: Boolean) {
|
||||||
|
_uiState.value = _uiState.value.copy(isAllApplicationsEnabled = isAllApplicationsEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onRemoveCheckedPackage(packageName: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(checkedPackageNames = _uiState.value.checkedPackageNames - packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getQueriedPackages(query: String): List<PackageInfo> {
|
||||||
|
return getAllInternetCapablePackages().filter {
|
||||||
|
getPackageLabel(it).lowercase().contains(query.lowercase())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPackageLabel(packageInfo: PackageInfo): String {
|
||||||
|
return packageInfo.applicationInfo.loadLabel(application.packageManager).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAllInternetCapablePackages(): List<PackageInfo> {
|
||||||
|
return getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPackagesHoldingPermissions(permissions: Array<String>): List<PackageInfo> {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
packageManager.getPackagesHoldingPermissions(
|
||||||
|
permissions, PackageManager.PackageInfoFlags.of(0L))
|
||||||
|
} else {
|
||||||
|
packageManager.getPackagesHoldingPermissions(permissions, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isAllApplicationsEnabled(): Boolean {
|
||||||
|
return _uiState.value.isAllApplicationsEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveConfig(tunnelConfig: TunnelConfig) =
|
||||||
|
viewModelScope.launch {
|
||||||
|
tunnelConfigRepository.save(tunnelConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) =
|
||||||
|
viewModelScope.launch {
|
||||||
|
if (tunnelConfig != null) {
|
||||||
|
saveConfig(tunnelConfig).join()
|
||||||
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
|
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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildPeerListFromProxyPeers(): List<Peer> {
|
||||||
|
return _uiState.value.proxyPeers.map {
|
||||||
|
val builder = Peer.Builder()
|
||||||
|
if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim())
|
||||||
|
if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim())
|
||||||
|
if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.trim())
|
||||||
|
if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.trim())
|
||||||
|
if (it.persistentKeepalive.isNotEmpty()) {
|
||||||
|
builder.parsePersistentKeepalive(it.persistentKeepalive.trim())
|
||||||
|
}
|
||||||
|
builder.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emptyCheckedPackagesList() {
|
||||||
|
_uiState.value = _uiState.value.copy(checkedPackageNames = emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildInterfaceListFromProxyInterface(): Interface {
|
||||||
|
val builder = Interface.Builder()
|
||||||
|
builder.parsePrivateKey(_uiState.value.interfaceProxy.privateKey.trim())
|
||||||
|
builder.parseAddresses(_uiState.value.interfaceProxy.addresses.trim())
|
||||||
|
builder.parseDnsServers(_uiState.value.interfaceProxy.dnsServers.trim())
|
||||||
|
if (_uiState.value.interfaceProxy.mtu.isNotEmpty())
|
||||||
|
builder.parseMtu(_uiState.value.interfaceProxy.mtu.trim())
|
||||||
|
if (_uiState.value.interfaceProxy.listenPort.isNotEmpty()) {
|
||||||
|
builder.parseListenPort(_uiState.value.interfaceProxy.listenPort.trim())
|
||||||
|
}
|
||||||
|
if (isAllApplicationsEnabled()) emptyCheckedPackagesList()
|
||||||
|
if (_uiState.value.include) builder.includeApplications(_uiState.value.checkedPackageNames)
|
||||||
|
if (!_uiState.value.include) builder.excludeApplications(_uiState.value.checkedPackageNames)
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSaveAllChanges(): Result<Event> {
|
||||||
|
return try {
|
||||||
|
val peerList = buildPeerListFromProxyPeers()
|
||||||
|
val wgInterface = buildInterfaceListFromProxyInterface()
|
||||||
|
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
||||||
|
val tunnelConfig =
|
||||||
|
_uiState.value.tunnel?.copy(
|
||||||
|
name = _uiState.value.tunnelName, wgQuick = config.toWgQuickString())
|
||||||
|
updateTunnelConfig(tunnelConfig)
|
||||||
|
Result.Success(Event.Message.ConfigSaved)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.Error(Event.Error.Exception(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPeerPublicKeyChange(index: Int, value: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(
|
||||||
|
proxyPeers =
|
||||||
|
_uiState.value.proxyPeers.update(
|
||||||
|
index, _uiState.value.proxyPeers[index].copy(publicKey = value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPreSharedKeyChange(index: Int, value: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(
|
||||||
|
proxyPeers =
|
||||||
|
_uiState.value.proxyPeers.update(
|
||||||
|
index, _uiState.value.proxyPeers[index].copy(preSharedKey = value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEndpointChange(index: Int, value: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(
|
||||||
|
proxyPeers =
|
||||||
|
_uiState.value.proxyPeers.update(
|
||||||
|
index, _uiState.value.proxyPeers[index].copy(endpoint = value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAllowedIpsChange(index: Int, value: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(
|
||||||
|
proxyPeers =
|
||||||
|
_uiState.value.proxyPeers.update(
|
||||||
|
index, _uiState.value.proxyPeers[index].copy(allowedIps = value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPersistentKeepaliveChanged(index: Int, value: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(
|
||||||
|
proxyPeers =
|
||||||
|
_uiState.value.proxyPeers.update(
|
||||||
|
index, _uiState.value.proxyPeers[index].copy(persistentKeepalive = value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDeletePeer(index: Int) {
|
||||||
|
_uiState.value = _uiState.value.copy(
|
||||||
|
proxyPeers = _uiState.value.proxyPeers.removeAt(index)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addEmptyPeer() {
|
||||||
|
_uiState.value = _uiState.value.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateKeyPair() {
|
||||||
|
val keyPair = KeyPair()
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(
|
||||||
|
interfaceProxy =
|
||||||
|
_uiState.value.interfaceProxy.copy(
|
||||||
|
privateKey = keyPair.privateKey.toBase64(),
|
||||||
|
publicKey = keyPair.publicKey.toBase64()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAddressesChanged(value: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onListenPortChanged(value: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDnsServersChanged(value: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onMtuChanged(value: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onInterfacePublicKeyChange(value: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPrivateKeyChange(value: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value))
|
||||||
|
if (NumberUtils.isValidKey(value)) {
|
||||||
|
val pair = KeyPair(Key.fromBase64(value))
|
||||||
|
onInterfacePublicKeyChange(pair.publicKey.toBase64())
|
||||||
|
} else {
|
||||||
|
onInterfacePublicKeyChange("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun emitQueriedPackages(query: String) {
|
||||||
|
val packages =
|
||||||
|
getAllInternetCapablePackages().filter {
|
||||||
|
getPackageLabel(it).lowercase().contains(query.lowercase())
|
||||||
|
}
|
||||||
|
_uiState.value = _uiState.value.copy(packages = packages)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,27 +8,36 @@ import android.os.Build
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
|
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredWidth
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.overscroll
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Create
|
import androidx.compose.material.icons.filled.Create
|
||||||
import androidx.compose.material.icons.filled.FileOpen
|
import androidx.compose.material.icons.filled.FileOpen
|
||||||
import androidx.compose.material.icons.filled.QrCode
|
import androidx.compose.material.icons.filled.QrCode
|
||||||
import androidx.compose.material.icons.rounded.Add
|
import androidx.compose.material.icons.rounded.Add
|
||||||
|
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.Delete
|
import androidx.compose.material.icons.rounded.Delete
|
||||||
import androidx.compose.material.icons.rounded.Edit
|
import androidx.compose.material.icons.rounded.Edit
|
||||||
|
@ -42,14 +51,18 @@ import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.MaterialTheme.typography
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
@ -61,13 +74,10 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
@ -79,526 +89,474 @@ import androidx.navigation.NavController
|
||||||
import com.journeyapps.barcodescanner.ScanContract
|
import com.journeyapps.barcodescanner.ScanContract
|
||||||
import com.journeyapps.barcodescanner.ScanOptions
|
import com.journeyapps.barcodescanner.ScanOptions
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
|
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Routes
|
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
|
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.brickRed
|
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.corn
|
import com.zaneschepke.wireguardautotunnel.ui.theme.corn
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.mint
|
import com.zaneschepke.wireguardautotunnel.ui.theme.mint
|
||||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(
|
fun MainScreen(
|
||||||
viewModel: MainViewModel = hiltViewModel(),
|
viewModel: MainViewModel = hiltViewModel(),
|
||||||
padding: PaddingValues,
|
padding: PaddingValues,
|
||||||
|
focusRequester: FocusRequester,
|
||||||
showSnackbarMessage: (String) -> Unit,
|
showSnackbarMessage: (String) -> Unit,
|
||||||
navController: NavController
|
navController: NavController
|
||||||
) {
|
) {
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val isVisible = rememberSaveable { mutableStateOf(true) }
|
val isVisible = rememberSaveable { mutableStateOf(true) }
|
||||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||||
|
|
||||||
val sheetState = rememberModalBottomSheetState()
|
val sheetState = rememberModalBottomSheetState()
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
var showPrimaryChangeAlertDialog by remember { mutableStateOf(false) }
|
|
||||||
val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf())
|
|
||||||
val handshakeStatus by viewModel.handshakeStatus.collectAsStateWithLifecycle(
|
|
||||||
HandshakeStatus.NOT_STARTED
|
|
||||||
)
|
|
||||||
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle(Tunnel.State.DOWN)
|
|
||||||
val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle("")
|
|
||||||
val settings by viewModel.settings.collectAsStateWithLifecycle()
|
|
||||||
val statistics by viewModel.statistics.collectAsStateWithLifecycle(null)
|
|
||||||
|
|
||||||
// Nested scroll for control FAB
|
var showPrimaryChangeAlertDialog by remember { mutableStateOf(false) }
|
||||||
val nestedScrollConnection =
|
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
||||||
remember {
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
object : NestedScrollConnection {
|
|
||||||
override fun onPreScroll(
|
LaunchedEffect(uiState.loading) {
|
||||||
available: Offset,
|
if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
source: NestedScrollSource
|
delay(Constants.FOCUS_REQUEST_DELAY)
|
||||||
): Offset {
|
focusRequester.requestFocus()
|
||||||
// Hide FAB
|
}
|
||||||
if (available.y < -1) {
|
}
|
||||||
isVisible.value = false
|
|
||||||
}
|
if (uiState.loading) {
|
||||||
// Show FAB
|
LoadingScreen()
|
||||||
if (available.y > 1) {
|
return
|
||||||
isVisible.value = true
|
}
|
||||||
}
|
|
||||||
return Offset.Zero
|
val tunnelFileImportResultLauncher =
|
||||||
}
|
rememberLauncherForActivityResult(
|
||||||
|
object : ActivityResultContracts.GetContent() {
|
||||||
|
override fun createIntent(context: Context, input: String): Intent {
|
||||||
|
val intent = super.createIntent(context, input)
|
||||||
|
|
||||||
|
/* AndroidTV now comes with stubs that do nothing but display a Toast less helpful than
|
||||||
|
* what we can do, so detect this and throw an exception that we can catch later. */
|
||||||
|
val activitiesToResolveIntent =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
context.packageManager.queryIntentActivities(
|
||||||
|
intent,
|
||||||
|
PackageManager.ResolveInfoFlags.of(
|
||||||
|
PackageManager.MATCH_DEFAULT_ONLY.toLong()))
|
||||||
|
} else {
|
||||||
|
context.packageManager.queryIntentActivities(
|
||||||
|
intent, PackageManager.MATCH_DEFAULT_ONLY)
|
||||||
|
}
|
||||||
|
if (activitiesToResolveIntent.all {
|
||||||
|
val name = it.activityInfo.packageName
|
||||||
|
name.startsWith(Constants.GOOGLE_TV_EXPLORER_STUB) ||
|
||||||
|
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
|
||||||
|
}) {
|
||||||
|
showSnackbarMessage(Event.Error.FileExplorerRequired.message)
|
||||||
|
}
|
||||||
|
return intent
|
||||||
}
|
}
|
||||||
}
|
}) { data ->
|
||||||
|
|
||||||
val tunnelFileImportResultLauncher =
|
|
||||||
rememberLauncherForActivityResult(
|
|
||||||
object : ActivityResultContracts.GetContent() {
|
|
||||||
override fun createIntent(
|
|
||||||
context: Context,
|
|
||||||
input: String
|
|
||||||
): Intent {
|
|
||||||
val intent = super.createIntent(context, input)
|
|
||||||
|
|
||||||
/* AndroidTV now comes with stubs that do nothing but display a Toast less helpful than
|
|
||||||
* what we can do, so detect this and throw an exception that we can catch later. */
|
|
||||||
val activitiesToResolveIntent =
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
context.packageManager.queryIntentActivities(
|
|
||||||
intent,
|
|
||||||
PackageManager.ResolveInfoFlags.of(
|
|
||||||
PackageManager.MATCH_DEFAULT_ONLY.toLong()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
context.packageManager.queryIntentActivities(
|
|
||||||
intent,
|
|
||||||
PackageManager.MATCH_DEFAULT_ONLY
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (activitiesToResolveIntent.all {
|
|
||||||
val name = it.activityInfo.packageName
|
|
||||||
name.startsWith(Constants.GOOGLE_TV_EXPLORER_STUB) || name.startsWith(
|
|
||||||
Constants.ANDROID_TV_EXPLORER_STUB
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
throw WgTunnelException(context.getString(R.string.no_file_explorer))
|
|
||||||
}
|
|
||||||
return intent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) { data ->
|
|
||||||
if (data == null) return@rememberLauncherForActivityResult
|
if (data == null) return@rememberLauncherForActivityResult
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch {
|
||||||
try {
|
viewModel.onTunnelFileSelected(data).let {
|
||||||
viewModel.onTunnelFileSelected(data)
|
when (it) {
|
||||||
} catch (e: WgTunnelException) {
|
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||||
showSnackbarMessage(e.message)
|
is Result.Success -> {}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val scanLauncher =
|
||||||
val scanLauncher =
|
rememberLauncherForActivityResult(
|
||||||
rememberLauncherForActivityResult(
|
contract = ScanContract(),
|
||||||
contract = ScanContract(),
|
onResult = {
|
||||||
onResult = {
|
if (it.contents != null) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
viewModel.onTunnelQrResult(it.contents).let { result ->
|
||||||
viewModel.onTunnelQrResult(it.contents)
|
when (result) {
|
||||||
} catch (e: Exception) {
|
is Result.Success -> {}
|
||||||
when (e) {
|
is Result.Error -> showSnackbarMessage(result.error.message)
|
||||||
is WgTunnelException -> {
|
}
|
||||||
showSnackbarMessage(e.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
showSnackbarMessage("No QR code scanned")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
|
|
||||||
if (showPrimaryChangeAlertDialog) {
|
AnimatedVisibility(showPrimaryChangeAlertDialog) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = {
|
onDismissRequest = { showPrimaryChangeAlertDialog = false },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.onDefaultTunnelChange(selectedTunnel)
|
||||||
showPrimaryChangeAlertDialog = false
|
showPrimaryChangeAlertDialog = false
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = {
|
|
||||||
scope.launch {
|
|
||||||
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)) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onTunnelToggle(
|
|
||||||
checked: Boolean,
|
|
||||||
tunnel: TunnelConfig
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showSnackbarMessage(e.message!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
modifier =
|
|
||||||
Modifier.pointerInput(Unit) {
|
|
||||||
detectTapGestures(onTap = {
|
|
||||||
selectedTunnel = null
|
selectedTunnel = null
|
||||||
})
|
}) {
|
||||||
|
Text(text = stringResource(R.string.okay))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
floatingActionButtonPosition = FabPosition.End,
|
dismissButton = {
|
||||||
floatingActionButton = {
|
TextButton(onClick = { showPrimaryChangeAlertDialog = false }) {
|
||||||
AnimatedVisibility(
|
Text(text = stringResource(R.string.cancel))
|
||||||
visible = isVisible.value,
|
}
|
||||||
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
},
|
||||||
exit = slideOutVertically(targetOffsetY = { it * 2 })
|
title = { Text(text = stringResource(R.string.primary_tunnel_change)) },
|
||||||
) {
|
text = { Text(text = stringResource(R.string.primary_tunnel_change_question)) })
|
||||||
val secondaryColor = MaterialTheme.colorScheme.secondary
|
}
|
||||||
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
|
||||||
var fobColor by remember { mutableStateOf(secondaryColor) }
|
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
|
||||||
FloatingActionButton(
|
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
|
||||||
modifier =
|
}
|
||||||
Modifier
|
|
||||||
.padding(bottom = 90.dp)
|
Scaffold(
|
||||||
.onFocusChanged {
|
modifier =
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
Modifier.pointerInput(Unit) {
|
||||||
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
detectTapGestures(
|
||||||
|
onTap = {
|
||||||
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) selectedTunnel = null
|
||||||
|
})
|
||||||
|
},
|
||||||
|
floatingActionButtonPosition = FabPosition.End,
|
||||||
|
topBar = {
|
||||||
|
if (uiState.settings.isAutoTunnelEnabled)
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.requiredWidth(LocalConfiguration.current.screenWidthDp.dp).padding(end = 5.dp)) {
|
||||||
|
Row {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Bolt,
|
||||||
|
stringResource(id = R.string.auto),
|
||||||
|
modifier = Modifier.size(25.dp),
|
||||||
|
tint = if(uiState.settings.isAutoTunnelPaused) Color.Gray else mint)
|
||||||
|
Text(
|
||||||
|
"Auto-tunneling: ${if(uiState.settings.isAutoTunnelPaused) "paused" else "active" }",
|
||||||
|
style = typography.bodyLarge,
|
||||||
|
modifier = Modifier.padding(start = 10.dp))
|
||||||
|
}
|
||||||
|
if(uiState.settings.isAutoTunnelPaused) TextButton(
|
||||||
|
onClick = { viewModel.resumeAutoTunneling() },
|
||||||
|
modifier = Modifier.padding(end = 10.dp)) {
|
||||||
|
Text("Resume")
|
||||||
|
} else TextButton(
|
||||||
|
onClick = { viewModel.pauseAutoTunneling() },
|
||||||
|
modifier = Modifier.padding(end = 10.dp)) {
|
||||||
|
Text("Pause")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isVisible.value,
|
||||||
|
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
||||||
|
exit = slideOutVertically(targetOffsetY = { it * 2 })) {
|
||||||
|
val secondaryColor = MaterialTheme.colorScheme.secondary
|
||||||
|
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||||
|
var fobColor by remember { mutableStateOf(secondaryColor) }
|
||||||
|
FloatingActionButton(
|
||||||
|
modifier =
|
||||||
|
(if (WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
||||||
|
uiState.tunnels.isEmpty())
|
||||||
|
Modifier.focusRequester(focusRequester)
|
||||||
|
else Modifier)
|
||||||
|
.padding(bottom = 90.dp)
|
||||||
|
.onFocusChanged {
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
|
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = { showBottomSheet = true },
|
||||||
showBottomSheet = true
|
containerColor = fobColor,
|
||||||
},
|
shape = RoundedCornerShape(16.dp)) {
|
||||||
containerColor = fobColor,
|
|
||||||
shape = RoundedCornerShape(16.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.Add,
|
imageVector = Icons.Rounded.Add,
|
||||||
contentDescription = stringResource(id = R.string.add_tunnel),
|
contentDescription = stringResource(id = R.string.add_tunnel),
|
||||||
tint = Color.DarkGray
|
tint = Color.DarkGray)
|
||||||
)
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}) { innerPadding ->
|
||||||
) {
|
AnimatedVisibility(uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) {
|
||||||
if (tunnels.isEmpty()) {
|
Column(
|
||||||
Column(
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
verticalArrangement = Arrangement.Center,
|
||||||
verticalArrangement = Arrangement.Center,
|
modifier = Modifier.fillMaxSize().padding(padding)) {
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(padding)
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
|
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (showBottomSheet) {
|
if (showBottomSheet) {
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = {
|
onDismissRequest = { showBottomSheet = false }, sheetState = sheetState) {
|
||||||
showBottomSheet = false
|
|
||||||
},
|
|
||||||
sheetState = sheetState
|
|
||||||
) {
|
|
||||||
// Sheet content
|
// Sheet content
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.fillMaxWidth()
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
showBottomSheet = false
|
|
||||||
try {
|
|
||||||
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showSnackbarMessage(e.message!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(10.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Filled.FileOpen,
|
|
||||||
contentDescription = stringResource(id = R.string.open_file),
|
|
||||||
modifier = Modifier.padding(10.dp)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
stringResource(id = R.string.add_tunnels_text),
|
|
||||||
modifier = Modifier.padding(10.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
|
||||||
Divider()
|
|
||||||
Row(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
.clickable {
|
||||||
scope.launch {
|
showBottomSheet = false
|
||||||
showBottomSheet = false
|
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
||||||
val scanOptions = ScanOptions()
|
|
||||||
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
|
||||||
scanOptions.setOrientationLocked(true)
|
|
||||||
scanOptions.setPrompt(context.getString(R.string.scanning_qr))
|
|
||||||
scanOptions.setBeepEnabled(false)
|
|
||||||
scanOptions.captureActivity =
|
|
||||||
CaptureActivityPortrait::class.java
|
|
||||||
scanLauncher.launch(scanOptions)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.padding(10.dp)
|
.padding(10.dp)) {
|
||||||
) {
|
Icon(
|
||||||
|
Icons.Filled.FileOpen,
|
||||||
|
contentDescription = stringResource(id = R.string.open_file),
|
||||||
|
modifier = Modifier.padding(10.dp))
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.add_tunnels_text),
|
||||||
|
modifier = Modifier.padding(10.dp))
|
||||||
|
}
|
||||||
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
|
Divider()
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
scope.launch {
|
||||||
|
showBottomSheet = false
|
||||||
|
val scanOptions = ScanOptions()
|
||||||
|
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
||||||
|
scanOptions.setOrientationLocked(true)
|
||||||
|
scanOptions.setPrompt(context.getString(R.string.scanning_qr))
|
||||||
|
scanOptions.setBeepEnabled(false)
|
||||||
|
scanOptions.captureActivity = CaptureActivityPortrait::class.java
|
||||||
|
scanLauncher.launch(scanOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(10.dp)) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.QrCode,
|
Icons.Filled.QrCode,
|
||||||
contentDescription = stringResource(id = R.string.qr_scan),
|
contentDescription = stringResource(id = R.string.qr_scan),
|
||||||
modifier = Modifier.padding(10.dp)
|
modifier = Modifier.padding(10.dp))
|
||||||
)
|
|
||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.add_from_qr),
|
stringResource(id = R.string.add_from_qr),
|
||||||
modifier = Modifier.padding(10.dp)
|
modifier = Modifier.padding(10.dp))
|
||||||
)
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Divider()
|
Divider()
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.fillMaxWidth()
|
||||||
.fillMaxWidth()
|
.clickable {
|
||||||
.clickable {
|
showBottomSheet = false
|
||||||
showBottomSheet = false
|
navController.navigate(
|
||||||
navController.navigate(
|
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}")
|
||||||
"${Routes.Config.name}/${Constants.MANUAL_TUNNEL_CONFIG_ID}"
|
}
|
||||||
)
|
.padding(10.dp)) {
|
||||||
}
|
Icon(
|
||||||
.padding(10.dp)
|
Icons.Filled.Create,
|
||||||
) {
|
contentDescription = stringResource(id = R.string.create_import),
|
||||||
Icon(
|
modifier = Modifier.padding(10.dp))
|
||||||
Icons.Filled.Create,
|
Text(
|
||||||
contentDescription = stringResource(id = R.string.create_import),
|
stringResource(id = R.string.create_import),
|
||||||
modifier = Modifier.padding(10.dp)
|
modifier = Modifier.padding(10.dp))
|
||||||
)
|
}
|
||||||
Text(
|
}
|
||||||
stringResource(id = R.string.create_import),
|
|
||||||
modifier = Modifier.padding(10.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Column(
|
|
||||||
|
LazyColumn(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.fillMaxWidth().fillMaxHeight(.90f).overscroll(ScrollableDefaults.overscrollEffect()).padding(innerPadding),
|
||||||
.fillMaxSize()
|
state = rememberLazyListState(0, uiState.tunnels.count()),
|
||||||
.padding(padding)
|
userScrollEnabled = true,
|
||||||
) {
|
reverseLayout = true,
|
||||||
LazyColumn(
|
flingBehavior = ScrollableDefaults.flingBehavior()) {
|
||||||
modifier =
|
items(uiState.tunnels,
|
||||||
Modifier
|
key = { tunnel -> tunnel.id }) { tunnel ->
|
||||||
.fillMaxSize()
|
val leadingIconColor =
|
||||||
.padding(top = 10.dp)
|
(if (uiState.vpnState.name == tunnel.name &&
|
||||||
.nestedScroll(nestedScrollConnection)
|
uiState.vpnState.status == Tunnel.State.UP) {
|
||||||
) {
|
uiState.vpnState.statistics
|
||||||
items(tunnels, key = { tunnel -> tunnel.id }) { tunnel ->
|
?.mapPeerStats()
|
||||||
val leadingIconColor = (
|
?.map { it.value?.handshakeStatus() }
|
||||||
if (tunnelName == tunnel.name) {
|
.let { statuses ->
|
||||||
when (handshakeStatus) {
|
when {
|
||||||
HandshakeStatus.HEALTHY -> mint
|
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> mint
|
||||||
HandshakeStatus.UNHEALTHY -> brickRed
|
statuses?.any { it == HandshakeStatus.STALE } == true -> corn
|
||||||
HandshakeStatus.STALE -> corn
|
statuses?.all { it == HandshakeStatus.NOT_STARTED } == true ->
|
||||||
HandshakeStatus.NOT_STARTED -> Color.Gray
|
Color.Gray
|
||||||
HandshakeStatus.NEVER_CONNECTED -> brickRed
|
else -> {
|
||||||
|
Color.Gray
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
Color.Gray
|
} else {
|
||||||
}
|
Color.Gray
|
||||||
)
|
})
|
||||||
val focusRequester = remember { FocusRequester() }
|
val expanded = remember { mutableStateOf(false) }
|
||||||
val expanded =
|
|
||||||
remember {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
RowListItem(
|
RowListItem(
|
||||||
icon = {
|
icon = {
|
||||||
if (settings.isTunnelConfigDefault(tunnel)) {
|
if (uiState.settings.isTunnelConfigDefault(tunnel)) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Star,
|
Icons.Rounded.Star,
|
||||||
stringResource(R.string.status),
|
stringResource(R.string.status),
|
||||||
tint = leadingIconColor,
|
tint = leadingIconColor,
|
||||||
modifier =
|
modifier = Modifier.padding(end = 10.dp).size(20.dp))
|
||||||
Modifier
|
} else {
|
||||||
.padding(end = 10.dp)
|
Icon(
|
||||||
.size(20.dp)
|
Icons.Rounded.Circle,
|
||||||
)
|
stringResource(R.string.status),
|
||||||
} else {
|
tint = leadingIconColor,
|
||||||
Icon(
|
modifier = Modifier.padding(end = 15.dp).size(15.dp))
|
||||||
Icons.Rounded.Circle,
|
}
|
||||||
stringResource(R.string.status),
|
|
||||||
tint = leadingIconColor,
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.padding(end = 15.dp)
|
|
||||||
.size(15.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
text = tunnel.name,
|
text = tunnel.name,
|
||||||
onHold = {
|
onHold = {
|
||||||
if ((state == Tunnel.State.UP) && (tunnel.name == tunnelName)) {
|
if ((uiState.vpnState.status == Tunnel.State.UP) &&
|
||||||
showSnackbarMessage(
|
(tunnel.name == uiState.vpnState.name)) {
|
||||||
context.resources.getString(R.string.turn_off_tunnel)
|
showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
||||||
)
|
return@RowListItem
|
||||||
return@RowListItem
|
}
|
||||||
}
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
selectedTunnel = tunnel
|
||||||
selectedTunnel = tunnel
|
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
if (state == Tunnel.State.UP && (tunnelName == tunnel.name)) {
|
if (uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
expanded.value = !expanded.value
|
(uiState.vpnState.name == tunnel.name)) {
|
||||||
}
|
expanded.value = !expanded.value
|
||||||
} else {
|
|
||||||
selectedTunnel = tunnel
|
|
||||||
focusRequester.requestFocus()
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
selectedTunnel = tunnel
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
statistics = statistics,
|
statistics = uiState.vpnState.statistics,
|
||||||
expanded = expanded.value,
|
expanded = expanded.value,
|
||||||
rowButton = {
|
rowButton = {
|
||||||
if (tunnel.id == selectedTunnel?.id && !WireGuardAutoTunnel.isRunningOnAndroidTv(
|
if (tunnel.id == selectedTunnel?.id &&
|
||||||
context
|
!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
)
|
Row {
|
||||||
) {
|
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
|
||||||
Row {
|
IconButton(
|
||||||
if (!settings.isTunnelConfigDefault(tunnel)) {
|
onClick = {
|
||||||
IconButton(onClick = {
|
if (uiState.settings.isAutoTunnelEnabled && !uiState.settings.isAutoTunnelPaused) {
|
||||||
if (settings.isAutoTunnelEnabled) {
|
showSnackbarMessage(
|
||||||
showSnackbarMessage(
|
Event.Message.AutoTunnelOffAction.message)
|
||||||
context.resources.getString(
|
} else {
|
||||||
R.string.turn_off_auto
|
showPrimaryChangeAlertDialog = true
|
||||||
)
|
}
|
||||||
)
|
|
||||||
} else {
|
|
||||||
showPrimaryChangeAlertDialog = true
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
Icons.Rounded.Star,
|
|
||||||
stringResource(id = R.string.set_primary)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
IconButton(onClick = {
|
|
||||||
navController.navigate(
|
|
||||||
"${Routes.Config.name}/${selectedTunnel?.id}"
|
|
||||||
)
|
|
||||||
}) {
|
}) {
|
||||||
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
|
Icon(
|
||||||
|
Icons.Rounded.Star,
|
||||||
|
stringResource(id = R.string.set_primary))
|
||||||
}
|
}
|
||||||
IconButton(
|
}
|
||||||
modifier = Modifier.focusable(),
|
IconButton(
|
||||||
onClick = { viewModel.onDelete(tunnel) }
|
onClick = {
|
||||||
) {
|
if (uiState.settings.isAutoTunnelEnabled && uiState.settings.isTunnelConfigDefault(tunnel)
|
||||||
Icon(
|
&& !uiState.settings.isAutoTunnelPaused) {
|
||||||
Icons.Rounded.Delete,
|
showSnackbarMessage(
|
||||||
stringResource(id = R.string.delete)
|
Event.Message.AutoTunnelOffAction.message)
|
||||||
)
|
} else navController.navigate(
|
||||||
}
|
"${Screen.Config.route}/${selectedTunnel?.id}")
|
||||||
}
|
}) {
|
||||||
} else {
|
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
|
||||||
val checked = state == Tunnel.State.UP && tunnel.name == tunnelName
|
}
|
||||||
if (!checked) expanded.value = false
|
IconButton(
|
||||||
|
modifier = Modifier.focusable(),
|
||||||
@Composable
|
onClick = { viewModel.onDelete(tunnel) }) {
|
||||||
fun TunnelSwitch() =
|
Icon(Icons.Rounded.Delete, stringResource(id = R.string.delete))
|
||||||
Switch(
|
}
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
|
||||||
checked = checked,
|
|
||||||
onCheckedChange = { checked ->
|
|
||||||
if (!checked) expanded.value = false
|
|
||||||
onTunnelToggle(checked, tunnel)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
|
||||||
Row {
|
|
||||||
if (!settings.isTunnelConfigDefault(tunnel)) {
|
|
||||||
IconButton(onClick = {
|
|
||||||
if (settings.isAutoTunnelEnabled) {
|
|
||||||
showSnackbarMessage(
|
|
||||||
context.resources.getString(
|
|
||||||
R.string.turn_off_auto
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
showPrimaryChangeAlertDialog = true
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
Icons.Rounded.Star,
|
|
||||||
stringResource(id = R.string.set_primary)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
IconButton(
|
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
|
||||||
onClick = {
|
|
||||||
if (state == Tunnel.State.UP && (tunnelName == tunnel.name)) {
|
|
||||||
expanded.value = !expanded.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Icon(Icons.Rounded.Info, stringResource(R.string.info))
|
|
||||||
}
|
|
||||||
IconButton(onClick = {
|
|
||||||
if (state == Tunnel.State.UP && tunnel.name == tunnelName) {
|
|
||||||
showSnackbarMessage(
|
|
||||||
context.resources.getString(
|
|
||||||
R.string.turn_off_tunnel
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
navController.navigate(
|
|
||||||
"${Routes.Config.name}/${tunnel.id}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
Icons.Rounded.Edit,
|
|
||||||
stringResource(id = R.string.edit)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
IconButton(onClick = {
|
|
||||||
if (state == Tunnel.State.UP && tunnel.name == tunnelName) {
|
|
||||||
showSnackbarMessage(
|
|
||||||
context.resources.getString(
|
|
||||||
R.string.turn_off_tunnel
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
viewModel.onDelete(tunnel)
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
Icons.Rounded.Delete,
|
|
||||||
stringResource(id = R.string.delete)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
TunnelSwitch()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
TunnelSwitch()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
)
|
val checked by remember {
|
||||||
}
|
derivedStateOf {
|
||||||
|
(uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
|
tunnel.name == uiState.vpnState.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!checked) expanded.value = false
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TunnelSwitch() =
|
||||||
|
Switch(
|
||||||
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = { checked ->
|
||||||
|
if (!checked) expanded.value = false
|
||||||
|
onTunnelToggle(checked, tunnel)
|
||||||
|
})
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
|
Row {
|
||||||
|
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (uiState.settings.isAutoTunnelEnabled) {
|
||||||
|
showSnackbarMessage(
|
||||||
|
Event.Message.AutoTunnelOffAction.message)
|
||||||
|
} else {
|
||||||
|
selectedTunnel = tunnel
|
||||||
|
showPrimaryChangeAlertDialog = true
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Star,
|
||||||
|
stringResource(id = R.string.set_primary))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
|
onClick = {
|
||||||
|
if (uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
|
(uiState.vpnState.name == tunnel.name)) {
|
||||||
|
expanded.value = !expanded.value
|
||||||
|
} else {
|
||||||
|
showSnackbarMessage(Event.Message.TunnelOnAction.message)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(Icons.Rounded.Info, stringResource(R.string.info))
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
|
tunnel.name == uiState.vpnState.name) {
|
||||||
|
showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
||||||
|
} else {
|
||||||
|
navController.navigate(
|
||||||
|
"${Screen.Config.route}/${tunnel.id}")
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
|
tunnel.name == uiState.vpnState.name) {
|
||||||
|
showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
||||||
|
} else {
|
||||||
|
viewModel.onDelete(tunnel)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Delete,
|
||||||
|
stringResource(id = R.string.delete))
|
||||||
|
}
|
||||||
|
TunnelSwitch()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TunnelSwitch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||||
|
|
||||||
|
data class MainUiState(
|
||||||
|
val settings : Settings = Settings(),
|
||||||
|
val tunnels : TunnelConfigs = emptyList(),
|
||||||
|
val vpnState: VpnState = VpnState(),
|
||||||
|
val loading : Boolean = true
|
||||||
|
)
|
|
@ -8,267 +8,254 @@ import android.provider.OpenableColumns
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceState
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceState
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
|
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
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.async
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class MainViewModel
|
class MainViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
private val tunnelRepo: TunnelConfigDao,
|
private val tunnelConfigRepository: TunnelConfigRepository,
|
||||||
private val settingsRepo: SettingsDoa,
|
private val settingsRepository: SettingsRepository,
|
||||||
private val vpnService: VpnService
|
private val vpnService: VpnService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val tunnels get() = tunnelRepo.getAllFlow()
|
|
||||||
val state get() = vpnService.state
|
|
||||||
|
|
||||||
val handshakeStatus get() = vpnService.handshakeStatus
|
val uiState =
|
||||||
val tunnelName get() = vpnService.tunnelName
|
combine(
|
||||||
private val _settings = MutableStateFlow(Settings())
|
settingsRepository.getSettingsFlow(),
|
||||||
val settings get() = _settings.asStateFlow()
|
tunnelConfigRepository.getTunnelConfigsFlow(),
|
||||||
val statistics get() = vpnService.statistics
|
vpnService.vpnState,
|
||||||
|
) { settings, tunnels, vpnState ->
|
||||||
|
validateWatcherServiceState(settings)
|
||||||
|
MainUiState(settings, tunnels, vpnState, false)
|
||||||
|
}
|
||||||
|
.stateIn(
|
||||||
|
viewModelScope,
|
||||||
|
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
||||||
|
MainUiState())
|
||||||
|
|
||||||
init {
|
private fun validateWatcherServiceState(settings: Settings) = viewModelScope.launch(Dispatchers.IO) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
val watcherState =
|
||||||
settingsRepo.getAllFlow().filter { it.isNotEmpty() }.collect {
|
ServiceManager.getServiceState(
|
||||||
val settings = it.first()
|
application.applicationContext, WireGuardConnectivityWatcherService::class.java)
|
||||||
validateWatcherServiceState(settings)
|
if (settings.isAutoTunnelEnabled &&
|
||||||
_settings.emit(settings)
|
watcherState == ServiceState.STOPPED) {
|
||||||
}
|
ServiceManager.startWatcherService(application.applicationContext)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun validateWatcherServiceState(settings: Settings) {
|
private fun stopWatcherService() = viewModelScope.launch(Dispatchers.IO) {
|
||||||
val watcherState =
|
ServiceManager.stopWatcherService(application.applicationContext)
|
||||||
ServiceManager.getServiceState(
|
|
||||||
application.applicationContext,
|
|
||||||
WireGuardConnectivityWatcherService::class.java
|
|
||||||
)
|
|
||||||
if (settings.isAutoTunnelEnabled && watcherState == ServiceState.STOPPED && settings.defaultTunnel != null) {
|
|
||||||
ServiceManager.startWatcherService(
|
|
||||||
application.applicationContext,
|
|
||||||
settings.defaultTunnel!!
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
fun onDelete(tunnel: TunnelConfig) {
|
||||||
fun onDelete(tunnel: TunnelConfig) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
viewModelScope.launch {
|
if (tunnelConfigRepository.count() == 1) {
|
||||||
if (tunnelRepo.count() == 1L) {
|
stopWatcherService()
|
||||||
ServiceManager.stopWatcherService(application.applicationContext)
|
val settings = settingsRepository.getSettings()
|
||||||
val settings = settingsRepo.getAll()
|
settings.defaultTunnel = null
|
||||||
if (settings.isNotEmpty()) {
|
settings.isAutoTunnelEnabled = false
|
||||||
val setting = settings[0]
|
settings.isAlwaysOnVpnEnabled = false
|
||||||
setting.defaultTunnel = null
|
saveSettings(settings)
|
||||||
setting.isAutoTunnelEnabled = false
|
}
|
||||||
setting.isAlwaysOnVpnEnabled = false
|
tunnelConfigRepository.delete(tunnel)
|
||||||
settingsRepo.save(setting)
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
}
|
|
||||||
}
|
|
||||||
tunnelRepo.delete(tunnel)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onTunnelStart(tunnelConfig: TunnelConfig) {
|
fun onTunnelStart(tunnelConfig: TunnelConfig) = viewModelScope.launch(Dispatchers.IO) {
|
||||||
viewModelScope.launch {
|
stopActiveTunnel().await()
|
||||||
stopActiveTunnel()
|
startTunnel(tunnelConfig)
|
||||||
startTunnel(tunnelConfig)
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startTunnel(tunnelConfig: TunnelConfig) {
|
private fun startTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch(Dispatchers.IO) {
|
||||||
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
|
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun stopActiveTunnel() {
|
private fun stopActiveTunnel() =
|
||||||
|
viewModelScope.async(Dispatchers.IO) {
|
||||||
if (ServiceManager.getServiceState(
|
if (ServiceManager.getServiceState(
|
||||||
application.applicationContext,
|
application.applicationContext, WireGuardTunnelService::class.java) ==
|
||||||
WireGuardTunnelService::class.java
|
ServiceState.STARTED) {
|
||||||
) == ServiceState.STARTED
|
onTunnelStop()
|
||||||
) {
|
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||||
onTunnelStop()
|
|
||||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTunnelStop() {
|
fun onTunnelStop() = viewModelScope.launch(Dispatchers.IO) {
|
||||||
ServiceManager.stopVpnService(application.applicationContext)
|
ServiceManager.stopVpnService(application.applicationContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validateConfigString(config: String) {
|
private fun validateConfigString(config: String) {
|
||||||
TunnelConfig.configFromQuick(config)
|
TunnelConfig.configFromQuick(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onTunnelQrResult(result: String) {
|
suspend fun onTunnelQrResult(result: String) : Result<Unit> {
|
||||||
try {
|
return try {
|
||||||
validateConfigString(result)
|
validateConfigString(result)
|
||||||
val tunnelConfig =
|
val tunnelConfig =
|
||||||
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
|
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
|
||||||
addTunnel(tunnelConfig)
|
addTunnel(tunnelConfig)
|
||||||
|
Result.Success(Unit)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw WgTunnelException(e)
|
Result.Error(Event.Error.InvalidQrCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveTunnelConfigFromStream(
|
private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) {
|
||||||
stream: InputStream,
|
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
|
||||||
fileName: String
|
val config = Config.parse(bufferReader)
|
||||||
) {
|
val tunnelName = getNameFromFileName(fileName)
|
||||||
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
|
addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
|
||||||
val config = Config.parse(bufferReader)
|
withContext(Dispatchers.IO) { stream.close() }
|
||||||
val tunnelName = getNameFromFileName(fileName)
|
}
|
||||||
addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
stream.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getInputStreamFromUri(uri: Uri): InputStream {
|
private fun getInputStreamFromUri(uri: Uri): InputStream? {
|
||||||
return application.applicationContext.contentResolver.openInputStream(uri)
|
return application.applicationContext.contentResolver.openInputStream(uri)
|
||||||
?: throw WgTunnelException(application.getString(R.string.stream_failed))
|
}
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun onTunnelFileSelected(uri: Uri) {
|
suspend fun onTunnelFileSelected(uri: Uri) : Result<Unit> {
|
||||||
try {
|
try {
|
||||||
val fileName = getFileName(application.applicationContext, uri)
|
if(isValidUriContentScheme(uri)){
|
||||||
val fileExtension = getFileExtensionFromFileName(fileName)
|
val fileName = getFileName(application.applicationContext, uri)
|
||||||
when (fileExtension) {
|
when (getFileExtensionFromFileName(fileName)) {
|
||||||
Constants.CONF_FILE_EXTENSION -> saveTunnelFromConfUri(fileName, uri)
|
Constants.CONF_FILE_EXTENSION -> saveTunnelFromConfUri(fileName, uri).let {
|
||||||
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
|
when(it) {
|
||||||
else -> throw WgTunnelException(
|
is Result.Error -> return Result.Error(Event.Error.FileReadFailed)
|
||||||
application.getString(R.string.file_extension_message)
|
is Result.Success -> return it
|
||||||
)
|
}
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw WgTunnelException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun saveTunnelsFromZipUri(uri: Uri) {
|
|
||||||
ZipInputStream(getInputStreamFromUri(uri)).use { zip ->
|
|
||||||
generateSequence { zip.nextEntry }
|
|
||||||
.filterNot {
|
|
||||||
it.isDirectory ||
|
|
||||||
getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
|
|
||||||
}
|
|
||||||
.forEach {
|
|
||||||
val name = getNameFromFileName(it.name)
|
|
||||||
val config = Config.parse(zip)
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
addTunnel(TunnelConfig(name = name, wgQuick = config.toWgQuickString()))
|
|
||||||
}
|
}
|
||||||
|
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
|
||||||
|
else -> return Result.Error(Event.Error.InvalidFileExtension)
|
||||||
}
|
}
|
||||||
}
|
return Result.Success(Unit)
|
||||||
}
|
} else {
|
||||||
|
return Result.Error(Event.Error.InvalidFileExtension)
|
||||||
private suspend fun saveTunnelFromConfUri(
|
|
||||||
name: String,
|
|
||||||
uri: Uri
|
|
||||||
) {
|
|
||||||
val stream = getInputStreamFromUri(uri)
|
|
||||||
saveTunnelConfigFromStream(stream, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
|
||||||
saveTunnel(tunnelConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
|
|
||||||
tunnelRepo.save(tunnelConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getFileNameByCursor(
|
|
||||||
context: Context,
|
|
||||||
uri: Uri
|
|
||||||
): String {
|
|
||||||
val cursor = context.contentResolver.query(uri, null, null, null, null)
|
|
||||||
if (cursor != null) {
|
|
||||||
cursor.use {
|
|
||||||
return getDisplayNameByCursor(it)
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
throw WgTunnelException("Failed to initialize cursor")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getDisplayNameColumnIndex(cursor: Cursor): Int {
|
|
||||||
val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
|
||||||
if (columnIndex == -1) {
|
|
||||||
throw WgTunnelException("Cursor out of bounds")
|
|
||||||
}
|
|
||||||
return columnIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getDisplayNameByCursor(cursor: Cursor): String {
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
val index = getDisplayNameColumnIndex(cursor)
|
|
||||||
return cursor.getString(index)
|
|
||||||
} else {
|
|
||||||
throw WgTunnelException("Cursor failed to move to first")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun validateUriContentScheme(uri: Uri) {
|
|
||||||
if (uri.scheme != Constants.URI_CONTENT_SCHEME) {
|
|
||||||
throw WgTunnelException(application.getString(R.string.file_extension_message))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getFileName(
|
|
||||||
context: Context,
|
|
||||||
uri: Uri
|
|
||||||
): String {
|
|
||||||
validateUriContentScheme(uri)
|
|
||||||
return try {
|
|
||||||
getFileNameByCursor(context, uri)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
NumberUtils.generateRandomTunnelName()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getNameFromFileName(fileName: String): String {
|
|
||||||
return fileName.substring(0, fileName.lastIndexOf('.'))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getFileExtensionFromFileName(fileName: String): String {
|
|
||||||
return try {
|
|
||||||
fileName.substring(fileName.lastIndexOf('.'))
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
""
|
return Result.Error(Event.Error.FileReadFailed)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun saveTunnelsFromZipUri(uri: Uri) {
|
||||||
|
ZipInputStream(getInputStreamFromUri(uri)).use { zip ->
|
||||||
|
generateSequence { zip.nextEntry }
|
||||||
|
.filterNot {
|
||||||
|
it.isDirectory || getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
|
||||||
|
}
|
||||||
|
.forEach {
|
||||||
|
val name = getNameFromFileName(it.name)
|
||||||
|
val config = Config.parse(zip)
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
addTunnel(TunnelConfig(name = name, wgQuick = config.toWgQuickString()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun saveTunnelFromConfUri(name: String, uri: Uri) : Result<Unit> {
|
||||||
|
val stream = getInputStreamFromUri(uri)
|
||||||
|
return if(stream != null) {
|
||||||
|
saveTunnelConfigFromStream(stream, name)
|
||||||
|
Result.Success(Unit)
|
||||||
|
} else {
|
||||||
|
Result.Error(Event.Error.FileReadFailed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
||||||
|
saveTunnel(tunnelConfig)
|
||||||
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pauseAutoTunneling() = viewModelScope.launch {
|
||||||
|
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) {
|
fun resumeAutoTunneling() = viewModelScope.launch {
|
||||||
if (selectedTunnel != null) {
|
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
|
||||||
_settings.emit(
|
|
||||||
_settings.value.copy(
|
|
||||||
defaultTunnel = selectedTunnel.toString()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
settingsRepo.save(_settings.value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
|
||||||
|
tunnelConfigRepository.save(tunnelConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFileNameByCursor(context: Context, uri: Uri): String? {
|
||||||
|
context.contentResolver.query(uri, null, null, null, null)?.use {
|
||||||
|
return getDisplayNameByCursor(it)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDisplayNameColumnIndex(cursor: Cursor): Int? {
|
||||||
|
val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||||
|
return if (columnIndex != -1) {
|
||||||
|
return columnIndex
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDisplayNameByCursor(cursor: Cursor): String? {
|
||||||
|
return if (cursor.moveToFirst()) {
|
||||||
|
val index = getDisplayNameColumnIndex(cursor)
|
||||||
|
if (index != null) {
|
||||||
|
cursor.getString(index)
|
||||||
|
} else null
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isValidUriContentScheme(uri: Uri): Boolean {
|
||||||
|
return uri.scheme == Constants.URI_CONTENT_SCHEME
|
||||||
|
}
|
||||||
|
private fun getFileName(context: Context, uri: Uri): String {
|
||||||
|
return getFileNameByCursor(context, uri) ?: NumberUtils.generateRandomTunnelName()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getNameFromFileName(fileName: String): String {
|
||||||
|
return fileName.substring(0, fileName.lastIndexOf('.'))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFileExtensionFromFileName(fileName: String): String {
|
||||||
|
return try {
|
||||||
|
fileName.substring(fileName.lastIndexOf('.'))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveSettings(settings: Settings) =
|
||||||
|
viewModelScope.launch(Dispatchers.IO) { settingsRepository.save(settings) }
|
||||||
|
|
||||||
|
fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) = viewModelScope.launch {
|
||||||
|
if (selectedTunnel != null) {
|
||||||
|
saveSettings(uiState.value.settings.copy(defaultTunnel = selectedTunnel.toString())).join()
|
||||||
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import android.Manifest
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.provider.Settings
|
import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
@ -30,6 +30,7 @@ import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.outlined.Add
|
import androidx.compose.material.icons.outlined.Add
|
||||||
import androidx.compose.material.icons.rounded.LocationOff
|
import androidx.compose.material.icons.rounded.LocationOff
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
@ -45,15 +46,13 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
|
||||||
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.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
@ -70,21 +69,21 @@ import com.wireguard.android.backend.Tunnel
|
||||||
import com.wireguard.android.backend.WgQuickBackend
|
import com.wireguard.android.backend.WgQuickBackend
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.datastore.DataStoreManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
import java.io.File
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
@OptIn(
|
@OptIn(
|
||||||
ExperimentalPermissionsApi::class,
|
ExperimentalPermissionsApi::class,
|
||||||
ExperimentalLayoutApi::class,
|
ExperimentalLayoutApi::class)
|
||||||
ExperimentalComposeUiApi::class
|
|
||||||
)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
viewModel: SettingsViewModel = hiltViewModel(),
|
viewModel: SettingsViewModel = hiltViewModel(),
|
||||||
|
@ -92,505 +91,418 @@ fun SettingsScreen(
|
||||||
showSnackbarMessage: (String) -> Unit,
|
showSnackbarMessage: (String) -> Unit,
|
||||||
focusRequester: FocusRequester
|
focusRequester: FocusRequester
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val scrollState = rememberScrollState()
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
||||||
val settings by viewModel.settings.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val trustedSSIDs by viewModel.trustedSSIDs.collectAsStateWithLifecycle()
|
|
||||||
val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf())
|
|
||||||
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
|
||||||
var currentText by remember { mutableStateOf("") }
|
|
||||||
val scrollState = rememberScrollState()
|
|
||||||
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
|
|
||||||
var showAuthPrompt by remember { mutableStateOf(false) }
|
|
||||||
var didExportFiles by remember { mutableStateOf(false) }
|
|
||||||
val isLocationDisclosureShown by viewModel.disclosureShown.collectAsStateWithLifecycle(
|
|
||||||
null
|
|
||||||
)
|
|
||||||
val vpnState = viewModel.vpnState.collectAsStateWithLifecycle(initialValue = Tunnel.State.DOWN)
|
|
||||||
|
|
||||||
val screenPadding = 5.dp
|
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
val fillMaxWidth = .85f
|
var currentText by remember { mutableStateOf("") }
|
||||||
|
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
|
||||||
|
var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
|
||||||
|
var didExportFiles by remember { mutableStateOf(false) }
|
||||||
|
var showAuthPrompt by remember { mutableStateOf(false) }
|
||||||
|
val focusRequester2 = remember { FocusRequester() }
|
||||||
|
|
||||||
fun setLocationDisclosureShown() = scope.launch {
|
val screenPadding = 5.dp
|
||||||
viewModel.dataStoreManager.saveToDataStore(
|
val fillMaxWidth = .85f
|
||||||
DataStoreManager.LOCATION_DISCLOSURE_SHOWN,
|
|
||||||
true
|
if (uiState.loading) {
|
||||||
)
|
LoadingScreen()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fun exportAllConfigs() {
|
||||||
|
try {
|
||||||
|
val files = uiState.tunnels.map { File(context.cacheDir, "${it.name}.conf") }
|
||||||
|
files.forEachIndexed { index, file ->
|
||||||
|
file.outputStream().use { it.write(uiState.tunnels[index].wgQuick.toByteArray()) }
|
||||||
|
}
|
||||||
|
FileUtils.saveFilesToZip(context, files)
|
||||||
|
didExportFiles = true
|
||||||
|
showSnackbarMessage(Event.Message.ConfigsExported.message)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showSnackbarMessage(Event.Error.Exception(e).message)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun exportAllConfigs() {
|
fun saveTrustedSSID() {
|
||||||
try {
|
if (currentText.isNotEmpty()) {
|
||||||
val files = tunnels.map { File(context.cacheDir, "${it.name}.conf") }
|
viewModel.onSaveTrustedSSID(currentText).let {
|
||||||
files.forEachIndexed { index, file ->
|
when(it) {
|
||||||
file.outputStream().use {
|
is Result.Success -> currentText = ""
|
||||||
it.write(tunnels[index].wgQuick.toByteArray())
|
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FileUtils.saveFilesToZip(context, files)
|
|
||||||
didExportFiles = true
|
|
||||||
showSnackbarMessage(context.getString(R.string.exported_configs_message))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showSnackbarMessage(e.message!!)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun saveTrustedSSID() {
|
fun openSettings() {
|
||||||
if (currentText.isNotEmpty()) {
|
scope.launch {
|
||||||
scope.launch {
|
val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||||
try {
|
intentSettings.data = Uri.fromParts("package", context.packageName, null)
|
||||||
viewModel.onSaveTrustedSSID(currentText)
|
context.startActivity(intentSettings)
|
||||||
currentText = ""
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showSnackbarMessage(e.message ?: context.getString(R.string.unknown_error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun isAllAutoTunnelPermissionsEnabled(): Boolean {
|
fun checkFineLocationGranted() {
|
||||||
return (
|
isBackgroundLocationGranted =
|
||||||
isBackgroundLocationGranted &&
|
|
||||||
fineLocationState.status.isGranted &&
|
|
||||||
!viewModel.isLocationServicesNeeded()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openSettings() {
|
|
||||||
scope.launch {
|
|
||||||
val intentSettings =
|
|
||||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
|
||||||
intentSettings.data =
|
|
||||||
Uri.fromParts("package", context.packageName, null)
|
|
||||||
context.startActivity(intentSettings)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
val backgroundLocationState =
|
|
||||||
rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
|
||||||
isBackgroundLocationGranted = if (!backgroundLocationState.status.isGranted) {
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
SideEffect {
|
|
||||||
setLocationDisclosureShown()
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
|
|
||||||
if (!fineLocationState.status.isGranted) {
|
if (!fineLocationState.status.isGranted) {
|
||||||
isBackgroundLocationGranted = false
|
false
|
||||||
} else {
|
} else {
|
||||||
SideEffect {
|
viewModel.setLocationDisclosureShown()
|
||||||
setLocationDisclosureShown()
|
true
|
||||||
}
|
|
||||||
isBackgroundLocationGranted = true
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
if(WireGuardAutoTunnel.isRunningOnAndroidTv() && Build.VERSION.SDK_INT == Build.VERSION_CODES.Q){
|
||||||
|
checkFineLocationGranted()
|
||||||
|
} else {
|
||||||
|
val backgroundLocationState =
|
||||||
|
rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
||||||
|
isBackgroundLocationGranted =
|
||||||
|
if (!backgroundLocationState.status.isGranted) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
SideEffect { viewModel.setLocationDisclosureShown() }
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
|
||||||
|
checkFineLocationGranted()
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(showLocationServicesAlertDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showLocationServicesAlertDialog = false },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
showLocationServicesAlertDialog = false
|
||||||
|
viewModel.toggleAutoTunnel()
|
||||||
|
}) {
|
||||||
|
Text(text = stringResource(R.string.okay))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showLocationServicesAlertDialog = false }) {
|
||||||
|
Text(text = stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = { Text(text = stringResource(R.string.location_services_not_detected)) },
|
||||||
|
text = { Text(text = stringResource(R.string.location_services_missing_message)) })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLocationDisclosureShown != true) {
|
if (!uiState.isLocationDisclosureShown) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier =
|
modifier = Modifier.fillMaxSize().verticalScroll(scrollState).padding(padding)) {
|
||||||
Modifier
|
Icon(
|
||||||
.fillMaxSize()
|
Icons.Rounded.LocationOff,
|
||||||
.verticalScroll(scrollState)
|
contentDescription = stringResource(id = R.string.map),
|
||||||
.padding(padding)
|
modifier = Modifier.padding(30.dp).size(128.dp))
|
||||||
) {
|
Text(
|
||||||
Icon(
|
stringResource(R.string.prominent_background_location_title),
|
||||||
Icons.Rounded.LocationOff,
|
textAlign = TextAlign.Center,
|
||||||
contentDescription = stringResource(id = R.string.map),
|
modifier = Modifier.padding(30.dp),
|
||||||
modifier =
|
fontSize = 20.sp)
|
||||||
Modifier
|
Text(
|
||||||
.padding(30.dp)
|
stringResource(R.string.prominent_background_location_message),
|
||||||
.size(128.dp)
|
textAlign = TextAlign.Center,
|
||||||
)
|
modifier = Modifier.padding(30.dp),
|
||||||
Text(
|
fontSize = 15.sp)
|
||||||
stringResource(R.string.prominent_background_location_title),
|
Row(
|
||||||
textAlign = TextAlign.Center,
|
modifier =
|
||||||
modifier = Modifier.padding(30.dp),
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
fontSize = 20.sp
|
Modifier.fillMaxWidth().padding(10.dp)
|
||||||
)
|
} else {
|
||||||
Text(
|
Modifier.fillMaxWidth().padding(30.dp)
|
||||||
stringResource(R.string.prominent_background_location_message),
|
},
|
||||||
textAlign = TextAlign.Center,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.padding(30.dp),
|
horizontalArrangement = Arrangement.SpaceEvenly) {
|
||||||
fontSize = 15.sp
|
TextButton(onClick = { viewModel.setLocationDisclosureShown() }) {
|
||||||
)
|
Text(stringResource(id = R.string.no_thanks))
|
||||||
Row(
|
}
|
||||||
modifier =
|
TextButton(
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
Modifier
|
onClick = {
|
||||||
.fillMaxWidth()
|
openSettings()
|
||||||
.padding(10.dp)
|
viewModel.setLocationDisclosureShown()
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(30.dp)
|
|
||||||
},
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
|
||||||
) {
|
|
||||||
TextButton(onClick = {
|
|
||||||
setLocationDisclosureShown()
|
|
||||||
}) {
|
}) {
|
||||||
Text(stringResource(id = R.string.no_thanks))
|
Text(stringResource(id = R.string.turn_on))
|
||||||
}
|
}
|
||||||
TextButton(modifier = Modifier.focusRequester(focusRequester), onClick = {
|
}
|
||||||
openSettings()
|
}
|
||||||
setLocationDisclosureShown()
|
}
|
||||||
}) {
|
|
||||||
Text(stringResource(id = R.string.turn_on))
|
if(showAuthPrompt) {
|
||||||
|
AuthorizationPrompt(
|
||||||
|
onSuccess = {
|
||||||
|
showAuthPrompt = false
|
||||||
|
exportAllConfigs()
|
||||||
|
},
|
||||||
|
onError = { _ ->
|
||||||
|
showAuthPrompt = false
|
||||||
|
showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
showAuthPrompt = false
|
||||||
|
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uiState.tunnels.isEmpty() && uiState.isLocationDisclosureShown) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier.fillMaxSize().padding(padding)) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.one_tunnel_required),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(15.dp),
|
||||||
|
fontStyle = FontStyle.Italic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (uiState.isLocationDisclosureShown && uiState.tunnels.isNotEmpty()) {
|
||||||
|
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 = 60.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(id = R.string.tunnel_on_wifi),
|
||||||
|
enabled =
|
||||||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
|
checked = uiState.settings.isTunnelOnWifiEnabled,
|
||||||
|
padding = screenPadding,
|
||||||
|
onCheckChanged = { viewModel.onToggleTunnelOnWifi() },
|
||||||
|
modifier = if(uiState.settings.isAutoTunnelEnabled) Modifier else Modifier.focusRequester(focusRequester).focusProperties { down = focusRequester2 })
|
||||||
|
AnimatedVisibility(visible = uiState.settings.isTunnelOnWifiEnabled) {
|
||||||
|
Column {
|
||||||
|
FlowRow(
|
||||||
|
modifier = Modifier.padding(screenPadding).fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(5.dp)) {
|
||||||
|
uiState.settings.trustedNetworkSSIDs.forEach { ssid ->
|
||||||
|
ClickableIconButton(
|
||||||
|
onClick = { if(WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
|
viewModel.onDeleteTrustedSSID(ssid)
|
||||||
|
focusRequester2.requestFocus()
|
||||||
|
}},
|
||||||
|
onIconClick = { viewModel.onDeleteTrustedSSID(ssid) },
|
||||||
|
text = ssid,
|
||||||
|
icon = Icons.Filled.Close,
|
||||||
|
enabled =
|
||||||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
|
uiState.settings.isAlwaysOnVpnEnabled))
|
||||||
|
}
|
||||||
|
if (uiState.settings.trustedNetworkSSIDs.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.none),
|
||||||
|
fontStyle = FontStyle.Italic,
|
||||||
|
color = Color.Gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OutlinedTextField(
|
||||||
|
enabled =
|
||||||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
|
value = currentText,
|
||||||
|
onValueChange = { currentText = it },
|
||||||
|
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
||||||
|
modifier =
|
||||||
|
Modifier.padding(
|
||||||
|
start = screenPadding, top = 5.dp, bottom = 10.dp)
|
||||||
|
.focusRequester(focusRequester2)
|
||||||
|
,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ConfigurationToggle(
|
||||||
|
stringResource(R.string.tunnel_mobile_data),
|
||||||
|
enabled =
|
||||||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
|
checked = uiState.settings.isTunnelOnMobileDataEnabled,
|
||||||
|
padding = screenPadding,
|
||||||
|
onCheckChanged = { viewModel.onToggleTunnelOnMobileData() })
|
||||||
|
ConfigurationToggle(
|
||||||
|
stringResource(id = R.string.tunnel_on_ethernet),
|
||||||
|
enabled =
|
||||||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
|
checked = uiState.settings.isTunnelOnEthernetEnabled,
|
||||||
|
padding = screenPadding,
|
||||||
|
onCheckChanged = { viewModel.onToggleTunnelOnEthernet() })
|
||||||
|
ConfigurationToggle(
|
||||||
|
stringResource(R.string.battery_saver),
|
||||||
|
enabled =
|
||||||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
|
checked = uiState.settings.isBatterySaverEnabled,
|
||||||
|
padding = screenPadding,
|
||||||
|
onCheckChanged = { viewModel.onToggleBatterySaver() })
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = (if(!uiState.settings.isAutoTunnelEnabled) Modifier else Modifier.focusRequester(focusRequester))
|
||||||
|
.fillMaxSize().padding(top = 5.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center) {
|
||||||
|
TextButton(
|
||||||
|
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
|
||||||
|
onClick = {
|
||||||
|
if (uiState.settings.isTunnelOnWifiEnabled && !uiState.settings.isAutoTunnelEnabled) {
|
||||||
|
when(false) {
|
||||||
|
isBackgroundLocationGranted ->
|
||||||
|
showSnackbarMessage(Event.Error.BackgroundLocationRequired.message)
|
||||||
|
fineLocationState.status.isGranted ->
|
||||||
|
showSnackbarMessage(Event.Error.PreciseLocationRequired.message)
|
||||||
|
viewModel.isLocationEnabled(context) ->
|
||||||
|
showLocationServicesAlertDialog = true
|
||||||
|
else -> {
|
||||||
|
viewModel.toggleAutoTunnel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
viewModel.toggleAutoTunnel()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
val autoTunnelButtonText =
|
||||||
|
if (uiState.settings.isAutoTunnelEnabled) {
|
||||||
|
stringResource(R.string.disable_auto_tunnel)
|
||||||
|
} else {
|
||||||
|
stringResource(id = R.string.enable_auto_tunnel)
|
||||||
|
}
|
||||||
|
Text(autoTunnelButtonText)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (WgQuickBackend.hasKernelSupport()) {
|
||||||
|
Surface(
|
||||||
|
tonalElevation = 2.dp,
|
||||||
|
shadowElevation = 2.dp,
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
modifier = Modifier.fillMaxWidth(fillMaxWidth).padding(vertical = 10.dp)) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier = Modifier.padding(15.dp)) {
|
||||||
|
SectionTitle(
|
||||||
|
title = stringResource(id = R.string.kernel), padding = screenPadding)
|
||||||
|
ConfigurationToggle(
|
||||||
|
stringResource(R.string.use_kernel),
|
||||||
|
enabled =
|
||||||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
|
uiState.settings.isAlwaysOnVpnEnabled ||
|
||||||
|
(uiState.vpnState.status == Tunnel.State.UP)),
|
||||||
|
checked = uiState.settings.isKernelEnabled,
|
||||||
|
padding = screenPadding,
|
||||||
|
onCheckChanged = { viewModel.onToggleKernelMode().let {
|
||||||
|
when(it) {
|
||||||
|
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||||
|
is Result.Success -> {}
|
||||||
|
}
|
||||||
|
} })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
}
|
|
||||||
|
|
||||||
if (showAuthPrompt) {
|
|
||||||
AuthorizationPrompt(
|
|
||||||
onSuccess = {
|
|
||||||
showAuthPrompt = false
|
|
||||||
exportAllConfigs()
|
|
||||||
},
|
|
||||||
onError = { error ->
|
|
||||||
showSnackbarMessage(error)
|
|
||||||
showAuthPrompt = false
|
|
||||||
},
|
|
||||||
onFailure = {
|
|
||||||
showAuthPrompt = false
|
|
||||||
showSnackbarMessage(context.getString(R.string.authentication_failed))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tunnels.isEmpty()) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(padding)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.one_tunnel_required),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.padding(15.dp),
|
|
||||||
fontStyle = FontStyle.Italic
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Top,
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.verticalScroll(scrollState)
|
|
||||||
.clickable(indication = null, interactionSource = interactionSource) {
|
|
||||||
focusManager.clearFocus()
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
shadowElevation = 2.dp,
|
shadowElevation = 2.dp,
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
modifier =
|
modifier =
|
||||||
(
|
Modifier.fillMaxWidth(fillMaxWidth)
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
|
||||||
Modifier
|
|
||||||
.height(IntrinsicSize.Min)
|
|
||||||
.fillMaxWidth(fillMaxWidth)
|
|
||||||
.padding(top = 10.dp)
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth(fillMaxWidth)
|
|
||||||
.padding(top = 60.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(id = R.string.tunnel_on_wifi),
|
|
||||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
|
||||||
checked = settings.isTunnelOnWifiEnabled,
|
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = {
|
|
||||||
scope.launch {
|
|
||||||
viewModel.onToggleTunnelOnWifi()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier.focusRequester(focusRequester)
|
|
||||||
)
|
|
||||||
AnimatedVisibility(visible = settings.isTunnelOnWifiEnabled) {
|
|
||||||
Column {
|
|
||||||
FlowRow(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(screenPadding)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(5.dp)
|
|
||||||
) {
|
|
||||||
trustedSSIDs.forEach { ssid ->
|
|
||||||
ClickableIconButton(
|
|
||||||
onIconClick = {
|
|
||||||
scope.launch {
|
|
||||||
viewModel.onDeleteTrustedSSID(ssid)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
text = ssid,
|
|
||||||
icon = Icons.Filled.Close,
|
|
||||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (trustedSSIDs.isEmpty()) {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.none),
|
|
||||||
fontStyle = FontStyle.Italic,
|
|
||||||
color = Color.Gray
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OutlinedTextField(
|
|
||||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
|
||||||
value = currentText,
|
|
||||||
onValueChange = { currentText = it },
|
|
||||||
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.padding(start = screenPadding, top = 5.dp, bottom = 10.dp)
|
|
||||||
.onFocusChanged {
|
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
|
||||||
keyboardController?.hide()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ConfigurationToggle(
|
|
||||||
stringResource(R.string.tunnel_mobile_data),
|
|
||||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
|
||||||
checked = settings.isTunnelOnMobileDataEnabled,
|
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = {
|
|
||||||
scope.launch {
|
|
||||||
viewModel.onToggleTunnelOnMobileData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
ConfigurationToggle(
|
|
||||||
stringResource(id = R.string.tunnel_on_ethernet),
|
|
||||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
|
||||||
checked = settings.isTunnelOnEthernetEnabled,
|
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = {
|
|
||||||
scope.launch {
|
|
||||||
viewModel.onToggleTunnelOnEthernet()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
ConfigurationToggle(
|
|
||||||
stringResource(R.string.battery_saver),
|
|
||||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
|
||||||
checked = settings.isBatterySaverEnabled,
|
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = {
|
|
||||||
scope.launch {
|
|
||||||
viewModel.onToggleBatterySaver()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(top = 5.dp),
|
|
||||||
horizontalArrangement = Arrangement.Center
|
|
||||||
) {
|
|
||||||
TextButton(
|
|
||||||
enabled = !settings.isAlwaysOnVpnEnabled,
|
|
||||||
onClick = {
|
|
||||||
if (!isAllAutoTunnelPermissionsEnabled() && settings.isTunnelOnWifiEnabled) {
|
|
||||||
val message =
|
|
||||||
if (!isBackgroundLocationGranted) {
|
|
||||||
context.getString(R.string.background_location_required)
|
|
||||||
} else if (viewModel.isLocationServicesNeeded()) {
|
|
||||||
context.getString(R.string.location_services_required)
|
|
||||||
} else {
|
|
||||||
context.getString(R.string.precise_location_required)
|
|
||||||
}
|
|
||||||
showSnackbarMessage(message)
|
|
||||||
} else {
|
|
||||||
scope.launch {
|
|
||||||
viewModel.toggleAutoTunnel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
val autoTunnelButtonText =
|
|
||||||
if (settings.isAutoTunnelEnabled) {
|
|
||||||
stringResource(R.string.disable_auto_tunnel)
|
|
||||||
} else {
|
|
||||||
stringResource(id = R.string.enable_auto_tunnel)
|
|
||||||
}
|
|
||||||
Text(autoTunnelButtonText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (WgQuickBackend.hasKernelSupport()) {
|
|
||||||
Surface(
|
|
||||||
tonalElevation = 2.dp,
|
|
||||||
shadowElevation = 2.dp,
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
color = MaterialTheme.colorScheme.surface,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth(fillMaxWidth)
|
|
||||||
.padding(vertical = 10.dp)
|
.padding(vertical = 10.dp)
|
||||||
) {
|
.padding(bottom = 140.dp)) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier.padding(15.dp)
|
modifier = Modifier.padding(15.dp)) {
|
||||||
) {
|
|
||||||
SectionTitle(
|
SectionTitle(
|
||||||
title = stringResource(id = R.string.kernel),
|
title = stringResource(id = R.string.other), padding = screenPadding)
|
||||||
padding = screenPadding
|
|
||||||
)
|
|
||||||
ConfigurationToggle(
|
|
||||||
stringResource(R.string.use_kernel),
|
|
||||||
enabled = !(
|
|
||||||
settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled ||
|
|
||||||
(vpnState.value == Tunnel.State.UP)
|
|
||||||
),
|
|
||||||
checked = settings.isKernelEnabled,
|
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = {
|
|
||||||
scope.launch {
|
|
||||||
try {
|
|
||||||
viewModel.onToggleKernelMode()
|
|
||||||
} catch (e: WgTunnelException) {
|
|
||||||
showSnackbarMessage(e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
|
||||||
Surface(
|
|
||||||
tonalElevation = 2.dp,
|
|
||||||
shadowElevation = 2.dp,
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
color = MaterialTheme.colorScheme.surface,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth(fillMaxWidth).padding(vertical = 10.dp)
|
|
||||||
.padding(bottom = 140.dp)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement = Arrangement.Top,
|
|
||||||
modifier = Modifier.padding(15.dp)
|
|
||||||
) {
|
|
||||||
SectionTitle(
|
|
||||||
title = stringResource(id = R.string.other),
|
|
||||||
padding = screenPadding
|
|
||||||
)
|
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(R.string.always_on_vpn_support),
|
stringResource(R.string.always_on_vpn_support),
|
||||||
enabled = !settings.isAutoTunnelEnabled,
|
enabled = !uiState.settings.isAutoTunnelEnabled,
|
||||||
checked = settings.isAlwaysOnVpnEnabled,
|
checked = uiState.settings.isAlwaysOnVpnEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = {
|
onCheckChanged = { viewModel.onToggleAlwaysOnVPN() })
|
||||||
scope.launch {
|
|
||||||
viewModel.onToggleAlwaysOnVPN()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(R.string.enabled_app_shortcuts),
|
stringResource(R.string.enabled_app_shortcuts),
|
||||||
enabled = true,
|
enabled = true,
|
||||||
checked = settings.isShortcutsEnabled,
|
checked = uiState.settings.isShortcutsEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = {
|
onCheckChanged = { viewModel.onToggleShortcutsEnabled() })
|
||||||
scope.launch {
|
|
||||||
viewModel.onToggleShortcutsEnabled()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier =
|
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
||||||
Modifier
|
horizontalArrangement = Arrangement.Center) {
|
||||||
.fillMaxSize()
|
TextButton(
|
||||||
.padding(top = 5.dp),
|
enabled = !didExportFiles, onClick = { showAuthPrompt = true }) {
|
||||||
horizontalArrangement = Arrangement.Center
|
Text(stringResource(R.string.export_configs))
|
||||||
) {
|
}
|
||||||
TextButton(
|
|
||||||
enabled = !didExportFiles,
|
|
||||||
onClick = {
|
|
||||||
showAuthPrompt = true
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.export_configs))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Spacer(modifier = Modifier.weight(.17f))
|
Spacer(modifier = Modifier.weight(.17f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
||||||
|
|
||||||
|
data class SettingsUiState(
|
||||||
|
val settings : Settings = Settings(),
|
||||||
|
val tunnels : List<TunnelConfig> = emptyList(),
|
||||||
|
val vpnState: VpnState = VpnState(),
|
||||||
|
val isLocationDisclosureShown : Boolean = true,
|
||||||
|
val loading : Boolean = true
|
||||||
|
)
|
|
@ -3,195 +3,171 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
import android.os.Build
|
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.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.datastore.DataStoreManager
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
|
||||||
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.WgTunnelException
|
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 dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SettingsViewModel
|
class SettingsViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
private val tunnelRepo: TunnelConfigDao,
|
private val tunnelConfigRepository: TunnelConfigRepository,
|
||||||
private val settingsRepo: SettingsDoa,
|
private val settingsRepository: SettingsRepository,
|
||||||
val dataStoreManager: DataStoreManager,
|
private val dataStoreManager: DataStoreManager,
|
||||||
private val rootShell: RootShell,
|
private val rootShell: RootShell,
|
||||||
private val vpnService: VpnService
|
private val vpnService: VpnService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _trustedSSIDs = MutableStateFlow(emptyList<String>())
|
val uiState = combine(
|
||||||
val trustedSSIDs = _trustedSSIDs.asStateFlow()
|
settingsRepository.getSettingsFlow(),
|
||||||
private val _settings = MutableStateFlow(Settings())
|
tunnelConfigRepository.getTunnelConfigsFlow(),
|
||||||
val settings get() = _settings.asStateFlow()
|
vpnService.vpnState,
|
||||||
val vpnState get() = vpnService.state
|
dataStoreManager.locationDisclosureFlow,
|
||||||
val tunnels get() = tunnelRepo.getAllFlow()
|
){ settings, tunnels, tunnelState, locationDisclosure ->
|
||||||
val disclosureShown = dataStoreManager.locationDisclosureFlow
|
SettingsUiState(settings, tunnels, tunnelState, locationDisclosure
|
||||||
|
?: false, false)
|
||||||
|
}.stateIn(viewModelScope,
|
||||||
|
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), SettingsUiState())
|
||||||
|
|
||||||
init {
|
fun onSaveTrustedSSID(ssid: String) : Result<Unit>{
|
||||||
isLocationServicesEnabled()
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
settingsRepo.getAllFlow().filter { it.isNotEmpty() }.collect {
|
|
||||||
val settings = it.first()
|
|
||||||
_settings.emit(settings)
|
|
||||||
_trustedSSIDs.emit(settings.trustedNetworkSSIDs.toList())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
suspend fun onSaveTrustedSSID(ssid: String) {
|
|
||||||
val trimmed = ssid.trim()
|
val trimmed = ssid.trim()
|
||||||
if (!_settings.value.trustedNetworkSSIDs.contains(trimmed)) {
|
return if (!uiState.value.settings.trustedNetworkSSIDs.contains(trimmed)) {
|
||||||
_settings.value.trustedNetworkSSIDs.add(trimmed)
|
uiState.value.settings.trustedNetworkSSIDs.add(trimmed)
|
||||||
settingsRepo.save(_settings.value)
|
saveSettings(uiState.value.settings)
|
||||||
|
Result.Success(Unit)
|
||||||
} else {
|
} else {
|
||||||
throw WgTunnelException("SSID already exists.")
|
Result.Error(Event.Error.SsidConflict)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onToggleTunnelOnMobileData() {
|
fun setLocationDisclosureShown() = viewModelScope.launch {
|
||||||
settingsRepo.save(
|
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, true)
|
||||||
_settings.value.copy(
|
}
|
||||||
isTunnelOnMobileDataEnabled = !_settings.value.isTunnelOnMobileDataEnabled
|
|
||||||
|
fun onToggleTunnelOnMobileData() {
|
||||||
|
saveSettings(
|
||||||
|
uiState.value.settings.copy(
|
||||||
|
isTunnelOnMobileDataEnabled = !uiState.value.settings.isTunnelOnMobileDataEnabled
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onDeleteTrustedSSID(ssid: String) {
|
fun onDeleteTrustedSSID(ssid: String) {
|
||||||
_settings.value.trustedNetworkSSIDs.remove(ssid)
|
saveSettings(uiState.value.settings.copy(
|
||||||
settingsRepo.save(_settings.value)
|
trustedNetworkSSIDs = (uiState.value.settings.trustedNetworkSSIDs - ssid).toMutableList()
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun emitFirstTunnelAsDefault() =
|
private suspend fun getDefaultTunnelOrFirst() : String {
|
||||||
viewModelScope.async {
|
return uiState.value.settings.defaultTunnel ?: tunnelConfigRepository.getAll().first().toString()
|
||||||
_settings.emit(_settings.value.copy(defaultTunnel = getFirstTunnelConfig().toString()))
|
}
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun toggleAutoTunnel() {
|
fun toggleAutoTunnel() = viewModelScope.launch {
|
||||||
if (_settings.value.isAutoTunnelEnabled) {
|
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
|
||||||
|
var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused
|
||||||
|
|
||||||
|
if (isAutoTunnelEnabled) {
|
||||||
ServiceManager.stopWatcherService(application)
|
ServiceManager.stopWatcherService(application)
|
||||||
} else {
|
} else {
|
||||||
if (_settings.value.defaultTunnel == null) {
|
ServiceManager.startWatcherService(application)
|
||||||
emitFirstTunnelAsDefault().await()
|
isAutoTunnelPaused = false
|
||||||
}
|
|
||||||
val defaultTunnel = _settings.value.defaultTunnel
|
|
||||||
ServiceManager.startWatcherService(application, defaultTunnel!!)
|
|
||||||
}
|
}
|
||||||
settingsRepo.save(
|
saveSettings(
|
||||||
_settings.value.copy(
|
uiState.value.settings.copy(
|
||||||
isAutoTunnelEnabled = !_settings.value.isAutoTunnelEnabled
|
isAutoTunnelEnabled = !isAutoTunnelEnabled,
|
||||||
|
isAutoTunnelPaused = isAutoTunnelPaused,
|
||||||
|
defaultTunnel = getDefaultTunnelOrFirst()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getFirstTunnelConfig(): TunnelConfig {
|
|
||||||
return tunnelRepo.getAll().first()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun onToggleAlwaysOnVPN() {
|
fun onToggleAlwaysOnVPN() = viewModelScope.launch {
|
||||||
if (_settings.value.defaultTunnel == null) {
|
val updatedSettings = uiState.value.settings.copy(
|
||||||
emitFirstTunnelAsDefault().await()
|
isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled,
|
||||||
}
|
defaultTunnel = getDefaultTunnelOrFirst()
|
||||||
val updatedSettings =
|
|
||||||
_settings.value.copy(
|
|
||||||
isAlwaysOnVpnEnabled = !_settings.value.isAlwaysOnVpnEnabled
|
|
||||||
)
|
)
|
||||||
emitSettings(updatedSettings)
|
|
||||||
saveSettings(updatedSettings)
|
saveSettings(updatedSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun emitSettings(settings: Settings) {
|
private fun saveSettings(settings: Settings) = viewModelScope.launch {
|
||||||
_settings.emit(
|
settingsRepository.save(settings)
|
||||||
settings
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveSettings(settings: Settings) {
|
fun onToggleTunnelOnEthernet() {
|
||||||
settingsRepo.save(settings)
|
saveSettings(uiState.value.settings.copy(
|
||||||
|
isTunnelOnEthernetEnabled = !uiState.value.settings.isTunnelOnEthernetEnabled
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onToggleTunnelOnEthernet() {
|
fun isLocationEnabled(context: Context): Boolean {
|
||||||
if (_settings.value.defaultTunnel == null) {
|
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||||
emitFirstTunnelAsDefault().await()
|
return LocationManagerCompat.isLocationEnabled(locationManager)
|
||||||
}
|
|
||||||
_settings.emit(
|
|
||||||
_settings.value.copy(
|
|
||||||
isTunnelOnEthernetEnabled = !_settings.value.isTunnelOnEthernetEnabled
|
|
||||||
)
|
|
||||||
)
|
|
||||||
settingsRepo.save(_settings.value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isLocationServicesEnabled(): Boolean {
|
fun onToggleShortcutsEnabled() {
|
||||||
val locationManager =
|
saveSettings(
|
||||||
application.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
uiState.value.settings.copy(
|
||||||
return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
|
isShortcutsEnabled = !uiState.value.settings.isShortcutsEnabled
|
||||||
}
|
|
||||||
|
|
||||||
fun isLocationServicesNeeded(): Boolean {
|
|
||||||
return (!isLocationServicesEnabled() && Build.VERSION.SDK_INT > Build.VERSION_CODES.P)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun onToggleShortcutsEnabled() {
|
|
||||||
settingsRepo.save(
|
|
||||||
_settings.value.copy(
|
|
||||||
isShortcutsEnabled = !_settings.value.isShortcutsEnabled
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onToggleBatterySaver() {
|
fun onToggleBatterySaver() {
|
||||||
settingsRepo.save(
|
saveSettings(
|
||||||
_settings.value.copy(
|
uiState.value.settings.copy(
|
||||||
isBatterySaverEnabled = !_settings.value.isBatterySaverEnabled
|
isBatterySaverEnabled = !uiState.value.settings.isBatterySaverEnabled
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveKernelMode(on: Boolean) {
|
private fun saveKernelMode(on: Boolean) {
|
||||||
settingsRepo.save(
|
saveSettings(
|
||||||
_settings.value.copy(
|
uiState.value.settings.copy(
|
||||||
isKernelEnabled = on
|
isKernelEnabled = on
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onToggleKernelMode() {
|
fun onToggleTunnelOnWifi() {
|
||||||
if (!_settings.value.isKernelEnabled) {
|
saveSettings(
|
||||||
|
uiState.value.settings.copy(
|
||||||
|
isTunnelOnWifiEnabled = !uiState.value.settings.isTunnelOnWifiEnabled
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onToggleKernelMode() : Result<Unit> {
|
||||||
|
if (!uiState.value.settings.isKernelEnabled) {
|
||||||
try {
|
try {
|
||||||
rootShell.start()
|
rootShell.start()
|
||||||
Timber.d("Root shell accepted!")
|
Timber.d("Root shell accepted!")
|
||||||
saveKernelMode(on = true)
|
saveKernelMode(on = true)
|
||||||
} catch (e: RootShell.RootShellException) {
|
} catch (e: RootShell.RootShellException) {
|
||||||
saveKernelMode(on = false)
|
saveKernelMode(on = false)
|
||||||
throw WgTunnelException("Root shell denied!")
|
return Result.Error(Event.Error.RootDenied)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
saveKernelMode(on = false)
|
saveKernelMode(on = false)
|
||||||
}
|
}
|
||||||
}
|
return Result.Success(Unit)
|
||||||
|
|
||||||
suspend fun onToggleTunnelOnWifi() {
|
|
||||||
settingsRepo.save(
|
|
||||||
_settings.value.copy(
|
|
||||||
isTunnelOnWifiEnabled = !_settings.value.isTunnelOnWifiEnabled
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
@ -48,197 +49,180 @@ import androidx.core.content.ContextCompat.startActivity
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsViewModel
|
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SupportScreen(
|
fun SupportScreen(
|
||||||
viewModel: SettingsViewModel = hiltViewModel(),
|
viewModel: SupportViewModel = hiltViewModel(),
|
||||||
padding: PaddingValues,
|
padding: PaddingValues,
|
||||||
|
showSnackbarMessage: (String) -> Unit,
|
||||||
focusRequester: FocusRequester
|
focusRequester: FocusRequester
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val fillMaxWidth = .85f
|
val fillMaxWidth = .85f
|
||||||
|
|
||||||
val settings by viewModel.settings.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
fun openWebPage(url: String) {
|
fun openWebPage(url: String) {
|
||||||
val webpage: Uri = Uri.parse(url)
|
try {
|
||||||
val intent = Intent(Intent.ACTION_VIEW, webpage)
|
val webpage: Uri = Uri.parse(url)
|
||||||
context.startActivity(intent)
|
val intent = Intent(Intent.ACTION_VIEW, webpage)
|
||||||
}
|
context.startActivity(intent)
|
||||||
|
} catch (e : Exception) {
|
||||||
|
showSnackbarMessage(Event.Error.Exception(e).message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun launchEmail() {
|
fun launchEmail() {
|
||||||
val intent =
|
try {
|
||||||
Intent(Intent.ACTION_SEND).apply {
|
val intent =
|
||||||
type = Constants.EMAIL_MIME_TYPE
|
Intent(Intent.ACTION_SEND).apply {
|
||||||
putExtra(Intent.EXTRA_EMAIL, context.getString(R.string.my_email))
|
type = Constants.EMAIL_MIME_TYPE
|
||||||
putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
|
putExtra(Intent.EXTRA_EMAIL, context.getString(R.string.my_email))
|
||||||
}
|
putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
|
||||||
startActivity(
|
}
|
||||||
context,
|
startActivity(context, createChooser(intent, context.getString(R.string.email_chooser)), null)
|
||||||
createChooser(intent, context.getString(R.string.email_chooser)),
|
} catch (e : Exception) {
|
||||||
null
|
showSnackbarMessage(Event.Error.Exception(e).message)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (uiState.loading) {
|
||||||
|
LoadingScreen()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.fillMaxSize()
|
||||||
.fillMaxSize()
|
.verticalScroll(rememberScrollState())
|
||||||
.verticalScroll(rememberScrollState())
|
.focusable()
|
||||||
.focusable()
|
.padding(padding)) {
|
||||||
.padding(padding)
|
Surface(
|
||||||
) {
|
tonalElevation = 2.dp,
|
||||||
Surface(
|
shadowElevation = 2.dp,
|
||||||
tonalElevation = 2.dp,
|
shape = RoundedCornerShape(12.dp),
|
||||||
shadowElevation = 2.dp,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
shape = RoundedCornerShape(12.dp),
|
modifier =
|
||||||
color = MaterialTheme.colorScheme.surface,
|
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
modifier =
|
Modifier.height(IntrinsicSize.Min)
|
||||||
(
|
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
|
||||||
Modifier
|
|
||||||
.height(IntrinsicSize.Min)
|
|
||||||
.fillMaxWidth(fillMaxWidth)
|
.fillMaxWidth(fillMaxWidth)
|
||||||
.padding(top = 10.dp)
|
.padding(top = 10.dp)
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 20.dp)
|
||||||
.fillMaxWidth(fillMaxWidth)
|
})
|
||||||
.padding(top = 20.dp)
|
.padding(bottom = 25.dp)) {
|
||||||
}
|
Column(modifier = Modifier.padding(20.dp)) {
|
||||||
).padding(bottom = 25.dp)
|
Text(
|
||||||
) {
|
stringResource(R.string.thank_you),
|
||||||
Column(modifier = Modifier.padding(20.dp)) {
|
textAlign = TextAlign.Start,
|
||||||
Text(
|
fontWeight = FontWeight.Bold,
|
||||||
stringResource(R.string.thank_you),
|
modifier = Modifier.padding(bottom = 20.dp),
|
||||||
textAlign = TextAlign.Start,
|
fontSize = 16.sp)
|
||||||
modifier = Modifier.padding(bottom = 20.dp),
|
Text(
|
||||||
fontSize = 16.sp
|
stringResource(id = R.string.support_help_text),
|
||||||
)
|
textAlign = TextAlign.Start,
|
||||||
Text(
|
fontSize = 16.sp,
|
||||||
stringResource(id = R.string.support_help_text),
|
modifier = Modifier.padding(bottom = 20.dp))
|
||||||
textAlign = TextAlign.Start,
|
TextButton(
|
||||||
fontSize = 16.sp,
|
onClick = { openWebPage(context.resources.getString(R.string.docs_url)) },
|
||||||
modifier = Modifier.padding(bottom = 20.dp)
|
modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester)) {
|
||||||
)
|
Row(
|
||||||
TextButton(onClick = {
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
openWebPage(context.resources.getString(R.string.docs_url))
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
}, modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester)) {
|
modifier = Modifier.fillMaxWidth()) {
|
||||||
Row(
|
Row {
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
Icon(Icons.Rounded.Book, stringResource(id = R.string.docs))
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
Text(
|
||||||
modifier = Modifier.fillMaxWidth()
|
stringResource(id = R.string.docs_description),
|
||||||
) {
|
textAlign = TextAlign.Justify,
|
||||||
Row {
|
modifier = Modifier.padding(start = 10.dp))
|
||||||
Icon(Icons.Rounded.Book, stringResource(id = R.string.docs))
|
}
|
||||||
Text(
|
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||||
stringResource(id = R.string.docs_description),
|
}
|
||||||
textAlign = TextAlign.Justify,
|
}
|
||||||
modifier = Modifier.padding(start = 10.dp)
|
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||||
)
|
TextButton(
|
||||||
}
|
onClick = { openWebPage(context.resources.getString(R.string.discord_url)) },
|
||||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
modifier = Modifier.padding(vertical = 5.dp)) {
|
||||||
}
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Row {
|
||||||
|
Icon(
|
||||||
|
imageVector = ImageVector.vectorResource(R.drawable.discord),
|
||||||
|
stringResource(id = R.string.discord),
|
||||||
|
Modifier.size(25.dp))
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.discord_description),
|
||||||
|
textAlign = TextAlign.Justify,
|
||||||
|
modifier = Modifier.padding(start = 10.dp))
|
||||||
|
}
|
||||||
|
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||||
|
TextButton(
|
||||||
|
onClick = { openWebPage(context.resources.getString(R.string.github_url)) },
|
||||||
|
modifier = Modifier.padding(vertical = 5.dp)) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Row {
|
||||||
|
Icon(
|
||||||
|
imageVector = ImageVector.vectorResource(R.drawable.github),
|
||||||
|
stringResource(id = R.string.github),
|
||||||
|
Modifier.size(25.dp))
|
||||||
|
Text(
|
||||||
|
"Open an issue",
|
||||||
|
textAlign = TextAlign.Justify,
|
||||||
|
modifier = Modifier.padding(start = 10.dp))
|
||||||
|
}
|
||||||
|
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||||
|
TextButton(
|
||||||
|
onClick = { launchEmail() }, modifier = Modifier.padding(vertical = 5.dp)) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Row {
|
||||||
|
Icon(Icons.Rounded.Mail, stringResource(id = R.string.email))
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.email_description),
|
||||||
|
textAlign = TextAlign.Justify,
|
||||||
|
modifier = Modifier.padding(start = 10.dp))
|
||||||
|
}
|
||||||
|
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
}
|
||||||
TextButton(
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
onClick = { openWebPage(context.resources.getString(R.string.discord_url)) },
|
Text(
|
||||||
modifier = Modifier.padding(vertical = 5.dp)
|
stringResource(id = R.string.privacy_policy),
|
||||||
) {
|
style = TextStyle(textDecoration = TextDecoration.Underline),
|
||||||
Row(
|
fontSize = 16.sp,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
modifier =
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
Modifier.clickable {
|
||||||
modifier = Modifier.fillMaxWidth()
|
openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
||||||
) {
|
})
|
||||||
Row {
|
Row(
|
||||||
Icon(
|
horizontalArrangement = Arrangement.spacedBy(25.dp),
|
||||||
imageVector = ImageVector.vectorResource(R.drawable.discord),
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
stringResource(
|
modifier = Modifier.padding(25.dp)) {
|
||||||
id = R.string.discord
|
Text("Version: ${BuildConfig.VERSION_NAME}", modifier = Modifier.focusable())
|
||||||
),
|
Text("Mode: ${if (uiState.settings.isKernelEnabled) "Kernel" else "Userspace" }")
|
||||||
Modifier.size(25.dp)
|
}
|
||||||
)
|
|
||||||
Text(
|
|
||||||
stringResource(id = R.string.discord_description),
|
|
||||||
textAlign = TextAlign.Justify,
|
|
||||||
modifier = Modifier.padding(start = 10.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
|
||||||
TextButton(
|
|
||||||
onClick = { openWebPage(context.resources.getString(R.string.github_url)) },
|
|
||||||
modifier = Modifier.padding(vertical = 5.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Row {
|
|
||||||
Icon(
|
|
||||||
imageVector = ImageVector.vectorResource(R.drawable.github),
|
|
||||||
stringResource(
|
|
||||||
id = R.string.github
|
|
||||||
),
|
|
||||||
Modifier.size(25.dp)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
"Open an issue",
|
|
||||||
textAlign = TextAlign.Justify,
|
|
||||||
modifier = Modifier.padding(start = 10.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
|
||||||
TextButton(
|
|
||||||
onClick = { launchEmail() },
|
|
||||||
modifier = Modifier.padding(vertical = 5.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Row {
|
|
||||||
Icon(Icons.Rounded.Mail, stringResource(id = R.string.email))
|
|
||||||
Text(
|
|
||||||
stringResource(id = R.string.email_description),
|
|
||||||
textAlign = TextAlign.Justify,
|
|
||||||
modifier = Modifier.padding(start = 10.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
}
|
||||||
Text(
|
|
||||||
stringResource(id = R.string.privacy_policy),
|
|
||||||
style = TextStyle(textDecoration = TextDecoration.Underline),
|
|
||||||
fontSize = 16.sp,
|
|
||||||
modifier =
|
|
||||||
Modifier.clickable {
|
|
||||||
openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(25.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.padding(25.dp)
|
|
||||||
) {
|
|
||||||
Text("Version: ${BuildConfig.VERSION_NAME}")
|
|
||||||
Text("Mode: ${if (settings.isKernelEnabled) "Kernel" else "Userspace" }")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.support
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
|
|
||||||
|
data class SupportUiState(
|
||||||
|
val settings : Settings = Settings(),
|
||||||
|
val loading : Boolean = true
|
||||||
|
)
|
|
@ -2,24 +2,24 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.support
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SupportViewModel @Inject constructor(
|
class SupportViewModel @Inject constructor(
|
||||||
private val settingsRepo: SettingsDoa
|
private val settingsRepository: SettingsRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _settings = MutableStateFlow(Settings())
|
|
||||||
val settings get() = _settings.asStateFlow()
|
val uiState = settingsRepository.getSettingsFlow().map {
|
||||||
init {
|
SupportUiState(it, false)
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
}.stateIn(
|
||||||
_settings.value = settingsRepo.getAll().first()
|
viewModelScope,
|
||||||
}
|
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
||||||
}
|
SupportUiState()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
package com.zaneschepke.wireguardautotunnel
|
package com.zaneschepke.wireguardautotunnel.util
|
||||||
|
|
||||||
object Constants {
|
object Constants {
|
||||||
const val MANUAL_TUNNEL_CONFIG_ID = "0"
|
const val MANUAL_TUNNEL_CONFIG_ID = "0"
|
||||||
const val WATCHER_SERVICE_WAKE_LOCK_TIMEOUT = 10 * 60 * 1000L // 10 minutes
|
const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1000L // 10 minutes
|
||||||
const val VPN_CONNECTIVITY_CHECK_INTERVAL = 3000L
|
const val DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT = 30 * 60 * 1000L // 30 minutes
|
||||||
const val VPN_STATISTIC_CHECK_INTERVAL = 1000L
|
const val VPN_STATISTIC_CHECK_INTERVAL = 1000L
|
||||||
const val TOGGLE_TUNNEL_DELAY = 500L
|
const val VPN_CONNECTED_NOTIFICATION_DELAY = 3000L
|
||||||
const val FADE_IN_ANIMATION_DURATION = 1000
|
const val TOGGLE_TUNNEL_DELAY = 300L
|
||||||
const val SLIDE_IN_ANIMATION_DURATION = 500
|
|
||||||
const val SLIDE_IN_TRANSITION_OFFSET = 1000
|
|
||||||
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"
|
||||||
|
@ -18,4 +16,7 @@ object Constants {
|
||||||
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
||||||
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
|
||||||
|
|
||||||
|
const val SUBSCRIPTION_TIMEOUT = 5_000L
|
||||||
|
const val FOCUS_REQUEST_DELAY = 500L
|
||||||
}
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.util
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
|
|
||||||
|
sealed class Event {
|
||||||
|
|
||||||
|
abstract val message: String
|
||||||
|
|
||||||
|
sealed class Error : Event() {
|
||||||
|
data object None : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_none)
|
||||||
|
}
|
||||||
|
data object SsidConflict : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_ssid_exists)
|
||||||
|
}
|
||||||
|
data object RootDenied : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_root_denied)
|
||||||
|
}
|
||||||
|
data class General(val customMessage: String) : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = customMessage
|
||||||
|
}
|
||||||
|
data class Exception(val exception : kotlin.Exception) : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = exception.message ?: WireGuardAutoTunnel.instance.getString(R.string.unknown_error)
|
||||||
|
}
|
||||||
|
data object InvalidQrCode : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_invalid_code)
|
||||||
|
}
|
||||||
|
data object InvalidFileExtension : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
|
||||||
|
}
|
||||||
|
data object FileReadFailed : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
|
||||||
|
}
|
||||||
|
data object AuthenticationFailed : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authentication_failed)
|
||||||
|
}
|
||||||
|
data object AuthorizationFailed : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authorization_failed)
|
||||||
|
}
|
||||||
|
data object BackgroundLocationRequired : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.background_location_required)
|
||||||
|
}
|
||||||
|
data object LocationServicesRequired : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.location_services_required)
|
||||||
|
}
|
||||||
|
data object PreciseLocationRequired : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.precise_location_required)
|
||||||
|
}
|
||||||
|
data object FileExplorerRequired : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_no_file_explorer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sealed class Message : Event() {
|
||||||
|
data object ConfigSaved: Message() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.config_changes_saved)
|
||||||
|
}
|
||||||
|
data object ConfigsExported: Message() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.exported_configs_message)
|
||||||
|
}
|
||||||
|
data object TunnelOffAction: Message() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_tunnel)
|
||||||
|
}
|
||||||
|
data object TunnelOnAction: Message() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_on_tunnel)
|
||||||
|
}
|
||||||
|
data object AutoTunnelOffAction: Message() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_auto)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.util
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import com.wireguard.android.backend.Statistics
|
||||||
|
import com.wireguard.android.backend.Statistics.PeerStats
|
||||||
|
import com.wireguard.crypto.Key
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import java.text.DecimalFormat
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
|
fun BroadcastReceiver.goAsync(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
block: suspend CoroutineScope.() -> Unit
|
||||||
|
) {
|
||||||
|
val pendingResult = goAsync()
|
||||||
|
@OptIn(DelicateCoroutinesApi::class) // Must run globally; there's no teardown callback.
|
||||||
|
GlobalScope.launch(context) {
|
||||||
|
try {
|
||||||
|
block()
|
||||||
|
} finally {
|
||||||
|
pendingResult.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BigDecimal.toThreeDecimalPlaceString(): String {
|
||||||
|
val df = DecimalFormat("#.###")
|
||||||
|
return df.format(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> List<T>.update(index: Int, item: T): List<T> = toMutableList().apply { this[index] = item }
|
||||||
|
fun <T> List<T>.removeAt(index: Int): List<T> = toMutableList().apply { this.removeAt(index) }
|
||||||
|
|
||||||
|
typealias TunnelConfigs = List<TunnelConfig>
|
||||||
|
typealias Packages = List<PackageInfo>
|
||||||
|
|
||||||
|
fun Statistics.mapPeerStats(): Map<Key, PeerStats?> {
|
||||||
|
return this.peers().associateWith { key ->
|
||||||
|
(this.peer(key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun PeerStats.latestHandshakeSeconds() : Long? {
|
||||||
|
return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun PeerStats.handshakeStatus() : HandshakeStatus {
|
||||||
|
//TODO add never connected status after duration
|
||||||
|
return this.latestHandshakeSeconds().let {
|
||||||
|
when {
|
||||||
|
it == null -> HandshakeStatus.NOT_STARTED
|
||||||
|
it <= HandshakeStatus.STALE_TIME_LIMIT_SEC -> HandshakeStatus.HEALTHY
|
||||||
|
it > HandshakeStatus.STALE_TIME_LIMIT_SEC -> HandshakeStatus.STALE
|
||||||
|
else -> {
|
||||||
|
HandshakeStatus.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.provider.MediaStore.MediaColumns
|
import android.provider.MediaStore.MediaColumns
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.util
|
||||||
|
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
||||||
|
sealed class Result<T> {
|
||||||
|
class Success<T>(val data: T): Result<T>()
|
||||||
|
class Error<T>(val error : Event.Error): Result<T>() {
|
||||||
|
init {
|
||||||
|
when(this.error) {
|
||||||
|
is Event.Error.Exception -> Timber.e(this.error.exception)
|
||||||
|
else -> Timber.e(this.error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,16 +0,0 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.util
|
|
||||||
|
|
||||||
import com.wireguard.config.BadConfigException
|
|
||||||
|
|
||||||
class WgTunnelException(e: Exception) : Exception() {
|
|
||||||
constructor(message: String) : this(Exception(message))
|
|
||||||
|
|
||||||
override val message: String = generateExceptionMessage(e)
|
|
||||||
|
|
||||||
private fun generateExceptionMessage(e: Exception): String {
|
|
||||||
return when (e) {
|
|
||||||
is BadConfigException -> "${e.section.name} ${e.location.name} ${e.reason.name}"
|
|
||||||
else -> e.message ?: "Unknown error occurred"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -9,13 +9,14 @@
|
||||||
<string name="github_url">https://github.com/zaneschepke/wgtunnel/issues</string>
|
<string name="github_url">https://github.com/zaneschepke/wgtunnel/issues</string>
|
||||||
<string name="docs_url">https://zaneschepke.com/wgtunnel-docs/overview.html</string>
|
<string name="docs_url">https://zaneschepke.com/wgtunnel-docs/overview.html</string>
|
||||||
<string name="privacy_policy_url">https://zaneschepke.com/wgtunnel-docs/privacypolicy.html</string>
|
<string name="privacy_policy_url">https://zaneschepke.com/wgtunnel-docs/privacypolicy.html</string>
|
||||||
<string name="file_extension_message">File is not a .conf or .zip</string>
|
<string name="error_file_extension">File is not a .conf or .zip</string>
|
||||||
<string name="turn_off_tunnel">Turn off tunnel before editing</string>
|
<string name="turn_off_tunnel">Action requires tunnel off</string>
|
||||||
<string name="no_tunnels">No tunnels added yet!</string>
|
<string name="no_tunnels">No tunnels added yet!</string>
|
||||||
<string name="tunnel_exists">Tunnel name already exists</string>
|
<string name="tunnel_exists">Tunnel name already exists</string>
|
||||||
<string name="discord_url">https://discord.gg/rbRRNh6H7V</string>
|
<string name="discord_url">https://discord.gg/rbRRNh6H7V</string>
|
||||||
<string name="watcher_notification_title">Watcher Service</string>
|
<string name="watcher_notification_title">Watcher Service</string>
|
||||||
<string name="watcher_notification_text">Monitoring network state changes</string>
|
<string name="watcher_notification_text_active">Monitoring network state changes: active</string>
|
||||||
|
<string name="watcher_notification_text_paused">Monitoring network state changes: paused</string>
|
||||||
<string name="tunnel_start_title">VPN Connected</string>
|
<string name="tunnel_start_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>
|
||||||
|
@ -78,7 +79,7 @@
|
||||||
<string name="lost_connection_failure_message">Attempting to reconnect to server after more than one minute of no response.</string>
|
<string name="lost_connection_failure_message">Attempting to reconnect to server after more than one minute of no response.</string>
|
||||||
<string name="always_on_vpn_support">Allow Always-On VPN </string>
|
<string name="always_on_vpn_support">Allow Always-On VPN </string>
|
||||||
<string name="select_tunnel_message">Please select a tunnel first</string>
|
<string name="select_tunnel_message">Please select a tunnel first</string>
|
||||||
<string name="location_services_not_detected">Unable to detect Location Services which are required for this feature. Please enable Location Services.</string>
|
<string name="location_services_not_detected">Location Services Not Detected</string>
|
||||||
<string name="check_again">Check again</string>
|
<string name="check_again">Check again</string>
|
||||||
<string name="detecting_location_services_disabled">Detecting Location Services disabled</string>
|
<string name="detecting_location_services_disabled">Detecting Location Services disabled</string>
|
||||||
<string name="precise_location_message">This feature requires precise location to access Wi-Fi SSID name. Please enable precise location here or in the app settings.</string>
|
<string name="precise_location_message">This feature requires precise location to access Wi-Fi SSID name. Please enable precise location here or in the app settings.</string>
|
||||||
|
@ -96,8 +97,6 @@
|
||||||
<string name="none">No trusted wifi names</string>
|
<string name="none">No trusted wifi names</string>
|
||||||
<string name="never">Never</string>
|
<string name="never">Never</string>
|
||||||
<string name="stream_failed">Failed to open file stream.</string>
|
<string name="stream_failed">Failed to open file stream.</string>
|
||||||
<string name="unknown_error_message">An unknown error occurred.</string>
|
|
||||||
<string name="no_file_app">No file app installed.</string>
|
|
||||||
<string name="other">Other</string>
|
<string name="other">Other</string>
|
||||||
<string name="auto_tunneling">Auto-tunneling</string>
|
<string name="auto_tunneling">Auto-tunneling</string>
|
||||||
<string name="select_tunnel">Select tunnel to use</string>
|
<string name="select_tunnel">Select tunnel to use</string>
|
||||||
|
@ -108,6 +107,7 @@
|
||||||
<string name="create_import">Create from scratch</string>
|
<string name="create_import">Create from scratch</string>
|
||||||
<string name="set_primary">Set primary</string>
|
<string name="set_primary">Set primary</string>
|
||||||
<string name="turn_off_auto">Action requires auto-tunnel disabled</string>
|
<string name="turn_off_auto">Action requires auto-tunnel disabled</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="info">Info</string>
|
<string name="info">Info</string>
|
||||||
<string name="done">Done</string>
|
<string name="done">Done</string>
|
||||||
|
@ -128,7 +128,8 @@
|
||||||
<string name="cancel">Cancel</string>
|
<string name="cancel">Cancel</string>
|
||||||
<string name="primary_tunnel_change">Primary tunnel change</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="primary_tunnel_change_question">Would you like to make this your primary tunnel?</string>
|
||||||
<string name="authentication_failed">Authentication failed</string>
|
<string name="error_authentication_failed">Authentication failed</string>
|
||||||
|
<string name="error_authorization_failed">Failed to authorize</string>
|
||||||
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
||||||
<string name="export_configs">Export configs</string>
|
<string name="export_configs">Export configs</string>
|
||||||
<string name="battery_saver">Battery saver (beta)</string>
|
<string name="battery_saver">Battery saver (beta)</string>
|
||||||
|
@ -137,7 +138,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="no_file_explorer">No file explorer installed</string>
|
|
||||||
<string name="status">status</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">zanecschepke@gmail.com</string>
|
<string name="my_email">zanecschepke@gmail.com</string>
|
||||||
|
@ -154,4 +154,14 @@
|
||||||
<string name="support_help_text">If you are experiencing issues, have improvement ideas, or just want to engage, the following resources are available:</string>
|
<string name="support_help_text">If you are experiencing issues, have improvement ideas, or just want to engage, the following resources are available:</string>
|
||||||
<string name="kernel">Kernel</string>
|
<string name="kernel">Kernel</string>
|
||||||
<string name="use_kernel">Use kernel module</string>
|
<string name="use_kernel">Use kernel module</string>
|
||||||
|
<string name="error_ssid_exists">SSID already exists</string>
|
||||||
|
<string name="error_root_denied">Root shell denied</string>
|
||||||
|
<string name="error_no_file_explorer">No file explorer installed</string>
|
||||||
|
<string name="error_no_scan">No code scanned</string>
|
||||||
|
<string name="error_invalid_code">Invalid QR code</string>
|
||||||
|
<string name="error_none">No error</string>
|
||||||
|
<string name="error_file_read">Failed to read file</string>
|
||||||
|
<string name="location_service_missing">Location Services Not Detected</string>
|
||||||
|
<string name="location_services_missing_message">The app is not detecting any location services enabled on your device. Depending on the device, this could cause the untrusted wifi feature to fail to read the wifi name. Would you like to continue anyways?</string>
|
||||||
|
<string name="auto_tunnel_title">Auto-tunnel Service</string>
|
||||||
</resources>
|
</resources>
|
|
@ -1,8 +1,9 @@
|
||||||
object Constants {
|
object Constants {
|
||||||
const val VERSION_NAME = "3.2.5"
|
const val VERSION_NAME = "3.3.0"
|
||||||
const val JVM_TARGET = "17"
|
const val JVM_TARGET = "17"
|
||||||
const val VERSION_CODE = 32500
|
const val VERSION_CODE = 33000
|
||||||
const val TARGET_SDK = 34
|
const val TARGET_SDK = 28
|
||||||
|
const val COMPILE_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"
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
Enhancements:
|
||||||
|
- Refactor state management
|
||||||
|
- Improve AndroidTV navigation
|
||||||
|
- Improve auto-tunneling efficiency
|
||||||
|
- Improve navigation
|
||||||
|
- Auto-tunneling pause feature
|
||||||
|
- Many bugfixes
|
||||||
|
|
Loading…
Reference in New Issue