diff --git a/.gitignore b/.gitignore
index c2d1e6a..fbd8370 100644
--- a/.gitignore
+++ b/.gitignore
@@ -71,3 +71,4 @@ app/release/output.json
.idea/codeStyles/
# where we keep our signing secrets locally
app/signing.properties
+/.kotlin/
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 5fbbaaf..24fbc6a 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -134,6 +134,7 @@ dependencies {
debugImplementation(libs.androidx.compose.manifest)
// get tunnel lib from github packages or mavenLocal
+// implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
implementation(libs.tunnel)
implementation(libs.amneziawg.android)
coreLibraryDesugaring(libs.desugar.jdk.libs)
diff --git a/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/9.json b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/9.json
new file mode 100644
index 0000000..48c0965
--- /dev/null
+++ b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/9.json
@@ -0,0 +1,197 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 9,
+ "identityHash": "e2c91dbf1885a9da592d3f54f1e08302",
+ "entities": [
+ {
+ "tableName": "Settings",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isAutoTunnelEnabled",
+ "columnName": "is_tunnel_enabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isTunnelOnMobileDataEnabled",
+ "columnName": "is_tunnel_on_mobile_data_enabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "trustedNetworkSSIDs",
+ "columnName": "trusted_network_ssids",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isAlwaysOnVpnEnabled",
+ "columnName": "is_always_on_vpn_enabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isTunnelOnEthernetEnabled",
+ "columnName": "is_tunnel_on_ethernet_enabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isShortcutsEnabled",
+ "columnName": "is_shortcuts_enabled",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ },
+ {
+ "fieldPath": "isTunnelOnWifiEnabled",
+ "columnName": "is_tunnel_on_wifi_enabled",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ },
+ {
+ "fieldPath": "isKernelEnabled",
+ "columnName": "is_kernel_enabled",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ },
+ {
+ "fieldPath": "isRestoreOnBootEnabled",
+ "columnName": "is_restore_on_boot_enabled",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ },
+ {
+ "fieldPath": "isMultiTunnelEnabled",
+ "columnName": "is_multi_tunnel_enabled",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ },
+ {
+ "fieldPath": "isAutoTunnelPaused",
+ "columnName": "is_auto_tunnel_paused",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ },
+ {
+ "fieldPath": "isPingEnabled",
+ "columnName": "is_ping_enabled",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ },
+ {
+ "fieldPath": "isAmneziaEnabled",
+ "columnName": "is_amnezia_enabled",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "TunnelConfig",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "wgQuick",
+ "columnName": "wg_quick",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tunnelNetworks",
+ "columnName": "tunnel_networks",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "isMobileDataTunnel",
+ "columnName": "is_mobile_data_tunnel",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ },
+ {
+ "fieldPath": "isPrimaryTunnel",
+ "columnName": "is_primary_tunnel",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ },
+ {
+ "fieldPath": "amQuick",
+ "columnName": "am_quick",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "isActive",
+ "columnName": "is_Active",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_TunnelConfig_name",
+ "unique": true,
+ "columnNames": [
+ "name"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
+ }
+ ],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e2c91dbf1885a9da592d3f54f1e08302')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0f76c92..bd23ec1 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -31,6 +31,12 @@
+
+
+
@@ -137,23 +143,23 @@
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt
index e47581b..b187aa5 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt
@@ -1,19 +1,22 @@
package com.zaneschepke.wireguardautotunnel
import android.app.Application
-import android.content.ComponentName
-import android.content.pm.PackageManager
import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
-import android.service.quicksettings.TileService
-import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
-import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
+import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
import dagger.hilt.android.HiltAndroidApp
+import kotlinx.coroutines.CoroutineScope
import timber.log.Timber
+import javax.inject.Inject
@HiltAndroidApp
class WireGuardAutoTunnel : Application() {
+
+ @Inject
+ @ApplicationScope
+ lateinit var applicationScope: CoroutineScope
+
override fun onCreate() {
super.onCreate()
instance = this
@@ -35,23 +38,5 @@ class WireGuardAutoTunnel : Application() {
companion object {
lateinit var instance: WireGuardAutoTunnel
private set
-
- fun isRunningOnAndroidTv(): Boolean {
- return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
- }
-
- fun requestTunnelTileServiceStateUpdate() {
- TileService.requestListeningState(
- instance,
- ComponentName(instance, TunnelControlTile::class.java),
- )
- }
-
- fun requestAutoTunnelTileServiceUpdate() {
- TileService.requestListeningState(
- instance,
- ComponentName(instance, AutoTunnelControlTile::class.java),
- )
- }
}
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt
index 721617e..679d3a2 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt
@@ -11,7 +11,7 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
@Database(
entities = [Settings::class, TunnelConfig::class],
- version = 8,
+ version = 9,
autoMigrations =
[
AutoMigration(from = 1, to = 2),
@@ -34,6 +34,7 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
spec = RemoveLegacySettingColumnsMigration::class,
),
AutoMigration(7, 8),
+ AutoMigration(8, 9),
],
exportSchema = true,
)
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/TunnelConfigDao.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/TunnelConfigDao.kt
index 0040808..cbb520a 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/TunnelConfigDao.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/TunnelConfigDao.kt
@@ -6,7 +6,7 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
-import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
+import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import kotlinx.coroutines.flow.Flow
@Dao
@@ -23,6 +23,9 @@ interface TunnelConfigDao {
@Query("SELECT * FROM TunnelConfig WHERE name=:name")
suspend fun getByName(name: String): TunnelConfig?
+ @Query("SELECT * FROM TunnelConfig WHERE is_Active=1")
+ suspend fun getActive(): TunnelConfigs
+
@Query("SELECT * FROM TunnelConfig")
suspend fun getAll(): TunnelConfigs
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/DataStoreManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/DataStoreManager.kt
index a608d92..852d356 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/DataStoreManager.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/DataStoreManager.kt
@@ -24,9 +24,7 @@ class DataStoreManager(
companion object {
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
- val TUNNEL_RUNNING_FROM_MANUAL_START =
- booleanPreferencesKey("TUNNEL_RUNNING_FROM_MANUAL_START")
- val ACTIVE_TUNNEL = intPreferencesKey("ACTIVE_TUNNEL")
+ val LAST_ACTIVE_TUNNEL = intPreferencesKey("LAST_ACTIVE_TUNNEL")
val CURRENT_SSID = stringPreferencesKey("CURRENT_SSID")
val IS_PIN_LOCK_ENABLED = booleanPreferencesKey("PIN_LOCK_ENABLED")
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/GeneralState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/GeneralState.kt
index 33ada27..6ab2566 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/GeneralState.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/GeneralState.kt
@@ -3,14 +3,12 @@ package com.zaneschepke.wireguardautotunnel.data.domain
data class GeneralState(
val isLocationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT,
val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
- val isTunnelRunningFromManualStart: Boolean = TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT,
- val activeTunnelId: Int? = null,
+ val lastActiveTunnelId: Int? = null,
) {
companion object {
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
- const val TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT = false
const val PIN_LOCK_ENABLED_DEFAULT = false
}
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt
index 77a9238..c225b2d 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt
@@ -32,8 +32,17 @@ data class TunnelConfig(
defaultValue = "",
)
val amQuick: String = AM_QUICK_DEFAULT,
+ @ColumnInfo(
+ name = "is_Active",
+ defaultValue = "false",
+ )
+ val isActive: Boolean = false,
) {
companion object {
+ fun findDefault(tunnels: List): TunnelConfig? {
+ return tunnels.find { it.isPrimaryTunnel } ?: tunnels.firstOrNull()
+ }
+
fun configFromWgQuick(wgQuick: String): Config {
val inputStream: InputStream = wgQuick.byteInputStream()
return inputStream.bufferedReader(Charsets.UTF_8).use {
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRepository.kt
index 9daf9a9..831f210 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRepository.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRepository.kt
@@ -7,8 +7,6 @@ interface AppDataRepository {
suspend fun getStartTunnelConfig(): TunnelConfig?
- suspend fun toggleWatcherServicePause()
-
val settings: SettingsRepository
val tunnels: TunnelConfigRepository
val appState: AppStateRepository
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRoomRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRoomRepository.kt
index 1214d82..521a9d4 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRoomRepository.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRoomRepository.kt
@@ -15,24 +15,8 @@ constructor(
}
override suspend fun getStartTunnelConfig(): TunnelConfig? {
- return if (appState.isTunnelRunningFromManualStart()) {
- appState.getActiveTunnelId()?.let {
- tunnels.getById(it)
- }
- } else {
- null
- }
- }
-
- override suspend fun toggleWatcherServicePause() {
- val settings = settings.getSettings()
- if (settings.isAutoTunnelEnabled) {
- val pauseAutoTunnel = !settings.isAutoTunnelPaused
- this.settings.save(
- settings.copy(
- isAutoTunnelPaused = pauseAutoTunnel,
- ),
- )
- }
+ return appState.getLastActiveTunnelId()?.let {
+ tunnels.getById(it)
+ } ?: getPrimaryOrFirstTunnel()
}
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppStateRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppStateRepository.kt
index 578bb8d..195bed4 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppStateRepository.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppStateRepository.kt
@@ -16,13 +16,9 @@ interface AppStateRepository {
suspend fun setBatteryOptimizationDisableShown(shown: Boolean)
- suspend fun isTunnelRunningFromManualStart(): Boolean
+ suspend fun getLastActiveTunnelId(): Int?
- suspend fun setTunnelRunningFromManualStart(id: Int)
-
- suspend fun setManualStop()
-
- suspend fun getActiveTunnelId(): Int?
+ suspend fun setLastActiveTunnelId(id: Int)
suspend fun getCurrentSsid(): String?
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt
index a1301cf..18ce1e9 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt
@@ -2,71 +2,65 @@ package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
+import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.withContext
import timber.log.Timber
-class DataStoreAppStateRepository(private val dataStoreManager: DataStoreManager) :
+class DataStoreAppStateRepository(
+ private val dataStoreManager: DataStoreManager,
+ @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
+) :
AppStateRepository {
override suspend fun isLocationDisclosureShown(): Boolean {
- return dataStoreManager.getFromStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN)
- ?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT
+ return withContext(ioDispatcher) {
+ dataStoreManager.getFromStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN)
+ ?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT
+ }
}
override suspend fun setLocationDisclosureShown(shown: Boolean) {
- dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, shown)
+ withContext(ioDispatcher) { dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, shown) }
}
override suspend fun isPinLockEnabled(): Boolean {
- return dataStoreManager.getFromStore(DataStoreManager.IS_PIN_LOCK_ENABLED)
- ?: GeneralState.PIN_LOCK_ENABLED_DEFAULT
+ return withContext(ioDispatcher) {
+ dataStoreManager.getFromStore(DataStoreManager.IS_PIN_LOCK_ENABLED)
+ ?: GeneralState.PIN_LOCK_ENABLED_DEFAULT
+ }
}
override suspend fun setPinLockEnabled(enabled: Boolean) {
- dataStoreManager.saveToDataStore(DataStoreManager.IS_PIN_LOCK_ENABLED, enabled)
+ withContext(ioDispatcher) { dataStoreManager.saveToDataStore(DataStoreManager.IS_PIN_LOCK_ENABLED, enabled) }
}
override suspend fun isBatteryOptimizationDisableShown(): Boolean {
- return dataStoreManager.getFromStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN)
- ?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT
+ return withContext(ioDispatcher) {
+ dataStoreManager.getFromStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN)
+ ?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT
+ }
}
override suspend fun setBatteryOptimizationDisableShown(shown: Boolean) {
- dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, shown)
+ withContext(ioDispatcher) { dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, shown) }
}
- override suspend fun isTunnelRunningFromManualStart(): Boolean {
- return dataStoreManager.getFromStore(DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START)
- ?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT
+ override suspend fun getLastActiveTunnelId(): Int? {
+ return withContext(ioDispatcher) { dataStoreManager.getFromStore(DataStoreManager.LAST_ACTIVE_TUNNEL) }
}
- override suspend fun setTunnelRunningFromManualStart(id: Int) {
- setTunnelRunningFromManualStart(true)
- setActiveTunnelId(id)
- }
-
- override suspend fun setManualStop() {
- setTunnelRunningFromManualStart(false)
- }
-
- private suspend fun setTunnelRunningFromManualStart(running: Boolean) {
- dataStoreManager.saveToDataStore(DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START, running)
- }
-
- override suspend fun getActiveTunnelId(): Int? {
- return dataStoreManager.getFromStore(DataStoreManager.ACTIVE_TUNNEL)
- }
-
- private suspend fun setActiveTunnelId(id: Int) {
- dataStoreManager.saveToDataStore(DataStoreManager.ACTIVE_TUNNEL, id)
+ override suspend fun setLastActiveTunnelId(id: Int) {
+ return withContext(ioDispatcher) { dataStoreManager.saveToDataStore(DataStoreManager.LAST_ACTIVE_TUNNEL, id) }
}
override suspend fun getCurrentSsid(): String? {
- return dataStoreManager.getFromStore(DataStoreManager.CURRENT_SSID)
+ return withContext(ioDispatcher) { dataStoreManager.getFromStore(DataStoreManager.CURRENT_SSID) }
}
override suspend fun setCurrentSsid(ssid: String) {
- dataStoreManager.saveToDataStore(DataStoreManager.CURRENT_SSID, ssid)
+ withContext(ioDispatcher) { dataStoreManager.saveToDataStore(DataStoreManager.CURRENT_SSID, ssid) }
}
override val generalStateFlow: Flow =
@@ -80,12 +74,10 @@ class DataStoreAppStateRepository(private val dataStoreManager: DataStoreManager
isBatteryOptimizationDisableShown =
pref[DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN]
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
- isTunnelRunningFromManualStart =
- pref[DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START]
- ?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
isPinLockEnabled =
pref[DataStoreManager.IS_PIN_LOCK_ENABLED]
- ?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
+ ?: GeneralState.PIN_LOCK_ENABLED_DEFAULT,
+ lastActiveTunnelId = pref[DataStoreManager.LAST_ACTIVE_TUNNEL],
)
} catch (e: IllegalArgumentException) {
Timber.e(e)
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomSettingsRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomSettingsRepository.kt
index de7abe1..3ecda21 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomSettingsRepository.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomSettingsRepository.kt
@@ -2,11 +2,16 @@ package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
+import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.withContext
-class RoomSettingsRepository(private val settingsDoa: SettingsDao) : SettingsRepository {
+class RoomSettingsRepository(private val settingsDoa: SettingsDao, @IoDispatcher private val ioDispatcher: CoroutineDispatcher) : SettingsRepository {
override suspend fun save(settings: Settings) {
- settingsDoa.save(settings)
+ withContext(ioDispatcher) {
+ settingsDoa.save(settings)
+ }
}
override fun getSettingsFlow(): Flow {
@@ -14,10 +19,12 @@ class RoomSettingsRepository(private val settingsDoa: SettingsDao) : SettingsRep
}
override suspend fun getSettings(): Settings {
- return settingsDoa.getAll().firstOrNull() ?: Settings()
+ return withContext(ioDispatcher) {
+ settingsDoa.getAll().firstOrNull() ?: Settings()
+ }
}
override suspend fun getAll(): List {
- return settingsDoa.getAll()
+ return withContext(ioDispatcher) { settingsDoa.getAll() }
}
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomTunnelConfigRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomTunnelConfigRepository.kt
index b0a6235..bf63974 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomTunnelConfigRepository.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomTunnelConfigRepository.kt
@@ -1,71 +1,97 @@
package com.zaneschepke.wireguardautotunnel.data.repository
+import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
-import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
+import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
+import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
+import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.withContext
-class RoomTunnelConfigRepository(private val tunnelConfigDao: TunnelConfigDao) :
+class RoomTunnelConfigRepository(
+ private val tunnelConfigDao: TunnelConfigDao,
+ @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
+) :
TunnelConfigRepository {
override fun getTunnelConfigsFlow(): Flow {
return tunnelConfigDao.getAllFlow()
}
override suspend fun getAll(): TunnelConfigs {
- return tunnelConfigDao.getAll()
+ return withContext(ioDispatcher) { tunnelConfigDao.getAll() }
}
override suspend fun save(tunnelConfig: TunnelConfig) {
- tunnelConfigDao.save(tunnelConfig)
+ withContext(ioDispatcher) {
+ tunnelConfigDao.save(tunnelConfig)
+ }.also {
+ WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
+ }
}
override suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?) {
- tunnelConfigDao.resetPrimaryTunnel()
- tunnelConfig?.let {
- save(
- it.copy(
- isPrimaryTunnel = true,
- ),
- )
+ withContext(ioDispatcher) {
+ tunnelConfigDao.resetPrimaryTunnel()
+ tunnelConfig?.let {
+ save(
+ it.copy(
+ isPrimaryTunnel = true,
+ ),
+ )
+ }
}
}
override suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?) {
- tunnelConfigDao.resetMobileDataTunnel()
- tunnelConfig?.let {
- save(
- it.copy(
- isMobileDataTunnel = true,
- ),
- )
+ withContext(ioDispatcher) {
+ tunnelConfigDao.resetMobileDataTunnel()
+ tunnelConfig?.let {
+ save(
+ it.copy(
+ isMobileDataTunnel = true,
+ ),
+ )
+ }
}
}
override suspend fun delete(tunnelConfig: TunnelConfig) {
- tunnelConfigDao.delete(tunnelConfig)
+ withContext(ioDispatcher) {
+ tunnelConfigDao.delete(tunnelConfig)
+ }.also {
+ WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
+ }
}
override suspend fun getById(id: Int): TunnelConfig? {
- return tunnelConfigDao.getById(id.toLong())
+ return withContext(ioDispatcher) { tunnelConfigDao.getById(id.toLong()) }
+ }
+
+ override suspend fun getActive(): TunnelConfigs {
+ return withContext(ioDispatcher) {
+ tunnelConfigDao.getActive()
+ }
}
override suspend fun count(): Int {
- return tunnelConfigDao.count().toInt()
+ return withContext(ioDispatcher) { tunnelConfigDao.count().toInt() }
}
override suspend fun findByTunnelName(name: String): TunnelConfig? {
- return tunnelConfigDao.getByName(name)
+ return withContext(ioDispatcher) { tunnelConfigDao.getByName(name) }
}
override suspend fun findByTunnelNetworksName(name: String): TunnelConfigs {
- return tunnelConfigDao.findByTunnelNetworkName(name)
+ return withContext(ioDispatcher) { tunnelConfigDao.findByTunnelNetworkName(name) }
}
override suspend fun findByMobileDataTunnel(): TunnelConfigs {
- return tunnelConfigDao.findByMobileDataTunnel()
+ return withContext(ioDispatcher) { tunnelConfigDao.findByMobileDataTunnel() }
}
override suspend fun findPrimary(): TunnelConfigs {
- return tunnelConfigDao.findByPrimary()
+ return withContext(ioDispatcher) { tunnelConfigDao.findByPrimary() }
}
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepository.kt
index bb19aa8..a823af5 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepository.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepository.kt
@@ -1,7 +1,7 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
-import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
+import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import kotlinx.coroutines.flow.Flow
interface TunnelConfigRepository {
@@ -19,6 +19,8 @@ interface TunnelConfigRepository {
suspend fun getById(id: Int): TunnelConfig?
+ suspend fun getActive(): TunnelConfigs
+
suspend fun count(): Int
suspend fun findByTunnelName(name: String): TunnelConfig?
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/RepositoryModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/RepositoryModule.kt
index a0476cf..23450d7 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/RepositoryModule.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/RepositoryModule.kt
@@ -54,14 +54,14 @@ class RepositoryModule {
@Singleton
@Provides
- fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao): TunnelConfigRepository {
- return RoomTunnelConfigRepository(tunnelConfigDao)
+ fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao, @IoDispatcher ioDispatcher: CoroutineDispatcher): TunnelConfigRepository {
+ return RoomTunnelConfigRepository(tunnelConfigDao, ioDispatcher)
}
@Singleton
@Provides
- fun provideSettingsRepository(settingsDao: SettingsDao): SettingsRepository {
- return RoomSettingsRepository(settingsDao)
+ fun provideSettingsRepository(settingsDao: SettingsDao, @IoDispatcher ioDispatcher: CoroutineDispatcher): SettingsRepository {
+ return RoomSettingsRepository(settingsDao, ioDispatcher)
}
@Singleton
@@ -72,8 +72,8 @@ class RepositoryModule {
@Provides
@Singleton
- fun provideGeneralStateRepository(dataStoreManager: DataStoreManager): AppStateRepository {
- return DataStoreAppStateRepository(dataStoreManager)
+ fun provideGeneralStateRepository(dataStoreManager: DataStoreManager, @IoDispatcher ioDispatcher: CoroutineDispatcher): AppStateRepository {
+ return DataStoreAppStateRepository(dataStoreManager, ioDispatcher)
}
@Provides
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt
index 06666e1..8d55e05 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt
@@ -3,12 +3,13 @@ package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.GoBackend
+import com.wireguard.android.backend.RootTunnelActionHandler
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell
import com.wireguard.android.util.ToolsInstaller
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
-import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
+import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
import dagger.Module
import dagger.Provides
@@ -29,24 +30,30 @@ class TunnelModule {
return RootShell(context)
}
+ @Provides
+ @Singleton
+ fun provideRootShellAm(@ApplicationContext context: Context): org.amnezia.awg.util.RootShell {
+ return org.amnezia.awg.util.RootShell(context)
+ }
+
@Provides
@Singleton
@Userspace
- fun provideUserspaceBackend(@ApplicationContext context: Context): Backend {
- return GoBackend(context)
+ fun provideUserspaceBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend {
+ return GoBackend(context, RootTunnelActionHandler(rootShell))
}
@Provides
@Singleton
@Kernel
fun provideKernelBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend {
- return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell))
+ return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell), RootTunnelActionHandler(rootShell))
}
@Provides
@Singleton
- fun provideAmneziaBackend(@ApplicationContext context: Context): org.amnezia.awg.backend.Backend {
- return org.amnezia.awg.backend.GoBackend(context)
+ fun provideAmneziaBackend(@ApplicationContext context: Context, rootShell: org.amnezia.awg.util.RootShell): org.amnezia.awg.backend.Backend {
+ return org.amnezia.awg.backend.GoBackend(context, org.amnezia.awg.backend.RootTunnelActionHandler(rootShell))
}
@Provides
@@ -58,7 +65,7 @@ class TunnelModule {
appDataRepository: AppDataRepository,
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
- ): VpnService {
+ ): TunnelService {
return WireGuardTunnel(
amneziaBackend,
userspaceBackend,
@@ -71,7 +78,7 @@ class TunnelModule {
@Provides
@Singleton
- fun provideServiceManager(appDataRepository: AppDataRepository, @IoDispatcher ioDispatcher: CoroutineDispatcher): ServiceManager {
- return ServiceManager(appDataRepository, ioDispatcher)
+ fun provideServiceManager(): ServiceManager {
+ return ServiceManager()
}
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BackgroundActionReceiver.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BackgroundActionReceiver.kt
new file mode 100644
index 0000000..de64fd0
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BackgroundActionReceiver.kt
@@ -0,0 +1,56 @@
+package com.zaneschepke.wireguardautotunnel.receiver
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
+import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
+import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+import javax.inject.Provider
+
+@AndroidEntryPoint
+class BackgroundActionReceiver : BroadcastReceiver() {
+
+ @Inject
+ @ApplicationScope
+ lateinit var applicationScope: CoroutineScope
+
+ @Inject
+ lateinit var tunnelService: Provider
+
+ @Inject
+ lateinit var tunnelConfigRepository: TunnelConfigRepository
+
+ override fun onReceive(context: Context, intent: Intent) {
+ val id = intent.getIntExtra(TUNNEL_ID_EXTRA_KEY, 0)
+ if (id == 0) return
+ when (intent.action) {
+ ACTION_CONNECT -> {
+ applicationScope.launch {
+ val tunnel = tunnelConfigRepository.getById(id)
+ tunnel?.let {
+ tunnelService.get().startTunnel(it)
+ }
+ }
+ }
+ ACTION_DISCONNECT -> {
+ applicationScope.launch {
+ val tunnel = tunnelConfigRepository.getById(id)
+ tunnel?.let {
+ tunnelService.get().stopTunnel(it)
+ }
+ }
+ }
+ }
+ }
+
+ companion object {
+ const val ACTION_CONNECT = "ACTION_CONNECT"
+ const val ACTION_DISCONNECT = "ACTION_DISCONNECT"
+ const val TUNNEL_ID_EXTRA_KEY = "tunnelId"
+ }
+}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt
index d214e19..aef58bf 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt
@@ -6,17 +6,22 @@ import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
+import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
+import javax.inject.Provider
@AndroidEntryPoint
class BootReceiver : BroadcastReceiver() {
@Inject
lateinit var appDataRepository: AppDataRepository
+ @Inject
+ lateinit var tunnelService: Provider
+
@Inject
lateinit var serviceManager: ServiceManager
@@ -24,32 +29,19 @@ class BootReceiver : BroadcastReceiver() {
@ApplicationScope
lateinit var applicationScope: CoroutineScope
- override fun onReceive(context: Context?, intent: Intent?) {
- if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return
- context?.run {
- applicationScope.launch {
- val settings = appDataRepository.settings.getSettings()
- if (settings.isRestoreOnBootEnabled) {
- if (settings.isAutoTunnelEnabled) {
- Timber.i("Starting watcher service from boot")
- serviceManager.startWatcherServiceForeground(context)
- }
- if (appDataRepository.appState.isTunnelRunningFromManualStart()) {
- appDataRepository.appState.getActiveTunnelId()?.let {
- Timber.i("Starting tunnel that was active before reboot")
- serviceManager.startVpnServiceForeground(
- context,
- appDataRepository.tunnels.getById(it)?.id,
- )
- return@launch
- }
- }
- if (settings.isAlwaysOnVpnEnabled) {
- Timber.i("Starting vpn service from boot AOVPN")
- serviceManager.startVpnServiceForeground(context)
- }
+ override fun onReceive(context: Context, intent: Intent) {
+ if (Intent.ACTION_BOOT_COMPLETED != intent.action) return
+ applicationScope.launch {
+ val settings = appDataRepository.settings.getSettings()
+ if (settings.isRestoreOnBootEnabled) {
+ appDataRepository.getStartTunnelConfig()?.let {
+ tunnelService.get().startTunnel(it)
}
}
+ if (settings.isAutoTunnelEnabled) {
+ Timber.i("Starting watcher service from boot")
+ serviceManager.startWatcherServiceForeground(context)
+ }
}
}
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/KernelReceiver.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/KernelReceiver.kt
new file mode 100644
index 0000000..980b62e
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/KernelReceiver.kt
@@ -0,0 +1,48 @@
+package com.zaneschepke.wireguardautotunnel.receiver
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
+import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
+import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
+import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+import javax.inject.Provider
+
+@AndroidEntryPoint
+class KernelReceiver : BroadcastReceiver() {
+
+ @Inject
+ lateinit var tunnelService: Provider
+
+ @Inject
+ @ApplicationScope
+ lateinit var applicationScope: CoroutineScope
+
+ @Inject
+ lateinit var tunnelConfigRepository: TunnelConfigRepository
+
+ override fun onReceive(context: Context, intent: Intent) {
+ val action = intent.action ?: return
+ applicationScope.launch {
+ if (action == REFRESH_TUNNELS_ACTION) {
+ tunnelService.get().runningTunnelNames().forEach { name ->
+ // TODO can optimize later
+ val tunnel = tunnelConfigRepository.findByTunnelName(name)
+ tunnel?.let {
+ tunnelConfigRepository.save(it.copy(isActive = true))
+ }
+ }
+ context.requestTunnelTileServiceStateUpdate()
+ }
+ }
+ }
+
+ companion object {
+ const val REFRESH_TUNNELS_ACTION = "com.wireguard.android.action.REFRESH_TUNNEL_STATES"
+ }
+}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt
deleted file mode 100644
index 9928ed4..0000000
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-package com.zaneschepke.wireguardautotunnel.receiver
-
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
-import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
-import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
-import com.zaneschepke.wireguardautotunnel.util.Constants
-import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import timber.log.Timber
-import javax.inject.Inject
-
-@AndroidEntryPoint
-class NotificationActionReceiver : BroadcastReceiver() {
- @Inject
- lateinit var settingsRepository: SettingsRepository
-
- @Inject
- lateinit var serviceManager: ServiceManager
-
- @Inject
- @ApplicationScope
- lateinit var applicationScope: CoroutineScope
-
- override fun onReceive(context: Context, intent: Intent?) {
- applicationScope.launch {
- try {
- // TODO fix for manual start changes when enabled
- serviceManager.stopVpnServiceForeground(context)
- delay(Constants.TOGGLE_TUNNEL_DELAY)
- serviceManager.startVpnServiceForeground(context)
- } catch (e: Exception) {
- Timber.e(e)
- } finally {
- cancel()
- }
- }
- }
-}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/AutoTunnelService.kt
similarity index 75%
rename from app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt
rename to app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/AutoTunnelService.kt
index 9f3f421..9f98b37 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/AutoTunnelService.kt
@@ -16,13 +16,11 @@ import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
+import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
-import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
@@ -32,9 +30,10 @@ import kotlinx.coroutines.withContext
import timber.log.Timber
import java.net.InetAddress
import javax.inject.Inject
+import javax.inject.Provider
@AndroidEntryPoint
-class WireGuardConnectivityWatcherService : ForegroundService() {
+class AutoTunnelService : ForegroundService() {
private val foregroundId = 122
@Inject
@@ -53,10 +52,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
lateinit var notificationService: NotificationService
@Inject
- lateinit var vpnService: VpnService
-
- @Inject
- lateinit var serviceManager: ServiceManager
+ lateinit var tunnelService: Provider
@Inject
@IoDispatcher
@@ -66,37 +62,43 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
@MainImmediateDispatcher
lateinit var mainImmediateDispatcher: CoroutineDispatcher
- private val networkEventsFlow = MutableStateFlow(WatcherState())
-
- private var watcherJob: Job? = null
+ private val networkEventsFlow = MutableStateFlow(AutoTunnelState())
private var wakeLock: PowerManager.WakeLock? = null
private val tag = this.javaClass.name
+ private var running: Boolean = false
+
override fun onCreate() {
super.onCreate()
lifecycleScope.launch(mainImmediateDispatcher) {
- try {
- if (appDataRepository.settings.getSettings().isAutoTunnelPaused) {
- launchWatcherPausedNotification()
- } else {
- launchWatcherNotification()
- }
- } catch (e: Exception) {
- Timber.e("Failed to start watcher service, not enough permissions")
+ kotlin.runCatching {
+ launchNotification()
+ }.onFailure {
+ Timber.e(it)
}
}
}
+ private suspend fun launchNotification() {
+ if (appDataRepository.settings.getSettings().isAutoTunnelPaused) {
+ launchWatcherPausedNotification()
+ } else {
+ launchWatcherNotification()
+ }
+ }
+
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()
+ if (running) return
+ kotlin.runCatching {
+ lifecycleScope.launch(mainImmediateDispatcher) {
+ launchNotification()
+ initWakeLock()
+ }
startWatcherJob()
- } catch (e: Exception) {
- Timber.e("Failed to launch watcher service, no permissions")
+ }.onFailure {
+ Timber.e(it)
}
}
@@ -107,8 +109,6 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
it.release()
}
}
- cancelWatcherJob()
- stopSelf()
}
private fun launchWatcherNotification(description: String = getString(R.string.watcher_notification_text_active)) {
@@ -145,49 +145,39 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
}
}
- private fun cancelWatcherJob() {
- try {
- watcherJob?.cancel()
- } catch (e: CancellationException) {
- Timber.i("Watcher job cancelled")
+ private fun startWatcherJob() = lifecycleScope.launch {
+ val setting = appDataRepository.settings.getSettings()
+ launch {
+ Timber.i("Starting wifi watcher")
+ watchForWifiConnectivityChanges()
}
- }
-
- private fun startWatcherJob() {
- watcherJob =
- lifecycleScope.launch {
- val setting = appDataRepository.settings.getSettings()
- launch {
- Timber.i("Starting wifi watcher")
- watchForWifiConnectivityChanges()
- }
- if (setting.isTunnelOnMobileDataEnabled) {
- launch {
- Timber.i("Starting mobile data watcher")
- watchForMobileDataConnectivityChanges()
- }
- }
- if (setting.isTunnelOnEthernetEnabled) {
- launch {
- Timber.i("Starting ethernet data watcher")
- watchForEthernetConnectivityChanges()
- }
- }
- launch {
- Timber.i("Starting settings watcher")
- watchForSettingsChanges()
- }
- if (setting.isPingEnabled) {
- launch {
- Timber.i("Starting ping watcher")
- watchForPingFailure()
- }
- }
- launch {
- Timber.i("Starting management watcher")
- manageVpn()
- }
+ if (setting.isTunnelOnMobileDataEnabled) {
+ launch {
+ Timber.i("Starting mobile data watcher")
+ watchForMobileDataConnectivityChanges()
}
+ }
+ if (setting.isTunnelOnEthernetEnabled) {
+ launch {
+ Timber.i("Starting ethernet data watcher")
+ watchForEthernetConnectivityChanges()
+ }
+ }
+ launch {
+ Timber.i("Starting settings watcher")
+ watchForSettingsChanges()
+ }
+ if (setting.isPingEnabled) {
+ launch {
+ Timber.i("Starting ping watcher")
+ watchForPingFailure()
+ }
+ }
+ launch {
+ Timber.i("Starting management watcher")
+ manageVpn()
+ }
+ running = true
}
private suspend fun watchForMobileDataConnectivityChanges() {
@@ -226,12 +216,11 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
}
private suspend fun watchForPingFailure() {
- val context = this
withContext(ioDispatcher) {
try {
do {
- if (vpnService.vpnState.value.status == TunnelState.UP) {
- val tunnelConfig = vpnService.vpnState.value.tunnelConfig
+ if (tunnelService.get().vpnState.value.status == TunnelState.UP) {
+ val tunnelConfig = tunnelService.get().vpnState.value.tunnelConfig
tunnelConfig?.let {
val config = TunnelConfig.configFromWgQuick(it.wgQuick)
val results =
@@ -253,9 +242,9 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
}
if (results.contains(false)) {
Timber.i("Restarting VPN for ping failure")
- serviceManager.stopVpnServiceForeground(context)
+ tunnelService.get().stopTunnel(it)
delay(Constants.VPN_RESTART_DELAY)
- serviceManager.startVpnServiceForeground(context, it.id)
+ tunnelService.get().startTunnel(it)
delay(Constants.PING_COOLDOWN)
}
}
@@ -379,63 +368,67 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
}
private fun isTunnelDown(): Boolean {
- return vpnService.vpnState.value.status == TunnelState.DOWN
+ return tunnelService.get().vpnState.value.status == TunnelState.DOWN
}
private suspend fun manageVpn() {
- val context = this
withContext(ioDispatcher) {
networkEventsFlow.collectLatest { watcherState ->
val autoTunnel = "Auto-tunnel watcher"
if (!watcherState.settings.isAutoTunnelPaused) {
// delay for rapid network state changes and then collect latest
delay(Constants.WATCHER_COLLECTION_DELAY)
- val tunnelConfig = vpnService.vpnState.value.tunnelConfig
+ val activeTunnel = tunnelService.get().vpnState.value.tunnelConfig
+ val defaultTunnel = appDataRepository.getPrimaryOrFirstTunnel()
when {
watcherState.isEthernetConditionMet() -> {
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
- if (isTunnelDown()) serviceManager.startVpnServiceForeground(context)
+ if (isTunnelDown()) {
+ defaultTunnel?.let {
+ tunnelService.get().startTunnel(it)
+ }
+ }
}
watcherState.isMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel on mobile data condition met")
val mobileDataTunnel = getMobileDataTunnel()
val tunnel =
- mobileDataTunnel ?: appDataRepository.getPrimaryOrFirstTunnel()
- if (isTunnelDown() || tunnelConfig?.isMobileDataTunnel == false) {
- serviceManager.startVpnServiceForeground(
- context,
- tunnel?.id,
- )
+ mobileDataTunnel ?: defaultTunnel
+ if (isTunnelDown() || activeTunnel?.isMobileDataTunnel == false) {
+ tunnel?.let {
+ tunnelService.get().startTunnel(it)
+ }
}
}
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
- if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
+ if (!isTunnelDown()) {
+ activeTunnel?.let {
+ tunnelService.get().stopTunnel(it)
+ }
+ }
}
watcherState.isUntrustedWifiConditionMet() -> {
- if (tunnelConfig?.tunnelNetworks?.contains(watcherState.currentNetworkSSID) == false ||
- tunnelConfig == null
+ if (activeTunnel?.tunnelNetworks?.contains(watcherState.currentNetworkSSID) == false ||
+ activeTunnel == null
) {
Timber.i(
"$autoTunnel - tunnel on ssid not associated with current tunnel condition met",
)
getSsidTunnel(watcherState.currentNetworkSSID)?.let {
Timber.i("Found tunnel associated with this SSID, bringing tunnel up: ${it.name}")
- if (isTunnelDown() || tunnelConfig?.id != it.id) {
- serviceManager.startVpnServiceForeground(
- context,
- it.id,
- )
+ if (isTunnelDown() || activeTunnel?.id != it.id) {
+ tunnelService.get().startTunnel(it)
}
} ?: suspend {
Timber.i("No tunnel associated with this SSID, using defaults")
val default = appDataRepository.getPrimaryOrFirstTunnel()
- if (default?.name != vpnService.name) {
+ if (default?.name != tunnelService.get().name || isTunnelDown()) {
default?.let {
- serviceManager.startVpnServiceForeground(context, it.id)
+ tunnelService.get().startTunnel(it)
}
}
}.invoke()
@@ -446,21 +439,21 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
Timber.i(
"$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off",
)
- if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
+ if (!isTunnelDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
}
watcherState.isTunnelOffOnWifiConditionMet() -> {
Timber.i(
"$autoTunnel - tunnel off on wifi condition met, turning vpn off",
)
- if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
+ if (!isTunnelDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
}
watcherState.isTunnelOffOnNoConnectivityMet() -> {
Timber.i(
"$autoTunnel - tunnel off on no connectivity met, turning vpn off",
)
- if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
+ if (!isTunnelDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
}
else -> {
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WatcherState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/AutoTunnelState.kt
similarity index 98%
rename from app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WatcherState.kt
rename to app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/AutoTunnelState.kt
index d4e7158..518b07e 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WatcherState.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/AutoTunnelState.kt
@@ -2,7 +2,7 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
-data class WatcherState(
+data class AutoTunnelState(
val isWifiConnected: Boolean = false,
val isEthernetConnected: Boolean = false,
val isMobileDataConnected: Boolean = false,
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt
index 9322855..e12e1ad 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt
@@ -3,17 +3,9 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.Service
import android.content.Context
import android.content.Intent
-import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
-import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
-import com.zaneschepke.wireguardautotunnel.util.Constants
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.withContext
import timber.log.Timber
-class ServiceManager(
- private val appDataRepository: AppDataRepository,
- @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
-) {
+class ServiceManager {
private fun actionOnService(action: Action, context: Context, cls: Class, extras: Map? = null) {
val intent =
Intent(context, cls).also {
@@ -35,67 +27,11 @@ class ServiceManager(
}
}
- suspend fun startVpnService(context: Context, tunnelId: Int? = null, isManualStart: Boolean = false) {
- if (isManualStart) onManualStart(tunnelId)
- actionOnService(
- Action.START,
- context,
- WireGuardTunnelService::class.java,
- tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
- )
- }
-
- suspend fun stopVpnServiceForeground(context: Context, isManualStop: Boolean = false) {
- withContext(ioDispatcher) {
- if (isManualStop) onManualStop()
- Timber.i("Stopping vpn service")
- actionOnService(
- Action.STOP_FOREGROUND,
- context,
- WireGuardTunnelService::class.java,
- )
- }
- }
-
- suspend fun stopVpnService(context: Context, isManualStop: Boolean = false) {
- withContext(ioDispatcher) {
- if (isManualStop) onManualStop()
- Timber.i("Stopping vpn service")
- actionOnService(
- Action.STOP,
- context,
- WireGuardTunnelService::class.java,
- )
- }
- }
-
- private suspend fun onManualStop() {
- appDataRepository.appState.setManualStop()
- }
-
- private suspend fun onManualStart(tunnelId: Int?) {
- tunnelId?.let {
- appDataRepository.appState.setTunnelRunningFromManualStart(it)
- }
- }
-
- suspend fun startVpnServiceForeground(context: Context, tunnelId: Int? = null, isManualStart: Boolean = false) {
- withContext(ioDispatcher) {
- if (isManualStart) onManualStart(tunnelId)
- actionOnService(
- Action.START_FOREGROUND,
- context,
- WireGuardTunnelService::class.java,
- tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
- )
- }
- }
-
fun startWatcherServiceForeground(context: Context) {
actionOnService(
Action.START_FOREGROUND,
context,
- WireGuardConnectivityWatcherService::class.java,
+ AutoTunnelService::class.java,
)
}
@@ -103,7 +39,7 @@ class ServiceManager(
actionOnService(
Action.START,
context,
- WireGuardConnectivityWatcherService::class.java,
+ AutoTunnelService::class.java,
)
}
@@ -111,7 +47,7 @@ class ServiceManager(
actionOnService(
Action.STOP,
context,
- WireGuardConnectivityWatcherService::class.java,
+ AutoTunnelService::class.java,
)
}
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt
deleted file mode 100644
index cf3667a..0000000
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt
+++ /dev/null
@@ -1,198 +0,0 @@
-package com.zaneschepke.wireguardautotunnel.service.foreground
-
-import android.app.PendingIntent
-import android.content.Intent
-import android.os.Bundle
-import androidx.core.app.ServiceCompat
-import androidx.lifecycle.lifecycleScope
-import com.zaneschepke.wireguardautotunnel.R
-import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
-import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
-import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher
-import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
-import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
-import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
-import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
-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 kotlinx.coroutines.CancellationException
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import timber.log.Timber
-import javax.inject.Inject
-
-@AndroidEntryPoint
-class WireGuardTunnelService : ForegroundService() {
- private val foregroundId = 123
-
- @Inject
- lateinit var vpnService: VpnService
-
- @Inject
- lateinit var appDataRepository: AppDataRepository
-
- @Inject
- lateinit var notificationService: NotificationService
-
- @Inject
- @MainImmediateDispatcher
- lateinit var mainImmediateDispatcher: CoroutineDispatcher
-
- @Inject
- @IoDispatcher
- lateinit var ioDispatcher: CoroutineDispatcher
-
- private var job: Job? = null
-
- private var didShowConnected = false
-
- override fun onCreate() {
- super.onCreate()
- lifecycleScope.launch(mainImmediateDispatcher) {
- // TODO fix this to not launch if AOVPN
- if (appDataRepository.tunnels.count() != 0) {
- launchVpnNotification()
- }
- }
- }
-
- override fun startService(extras: Bundle?) {
- super.startService(extras)
- cancelJob()
- job =
- lifecycleScope.launch {
- launch {
- val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY)
- if (vpnService.getState() == TunnelState.UP) {
- vpnService.stopTunnel()
- }
- vpnService.startTunnel(
- tunnelId?.let {
- appDataRepository.tunnels.getById(it)
- },
- )
- }
- launch {
- handshakeNotifications()
- }
- }
- }
-
- // TODO improve tunnel notifications
- private suspend fun handshakeNotifications() {
- withContext(ioDispatcher) {
- var tunnelName: String? = null
- vpnService.vpnState.collect { state ->
- state.statistics
- ?.mapPeerStats()
- ?.map { it.value?.handshakeStatus() }
- .let { statuses ->
- when {
- statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
- if (!didShowConnected) {
- delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
- tunnelName = state.tunnelConfig?.name
- launchVpnNotification(
- getString(R.string.tunnel_start_title),
- "${getString(R.string.tunnel_start_text)} - $tunnelName",
- )
- didShowConnected = true
- }
- }
-
- statuses?.any { it == HandshakeStatus.STALE } == true -> {}
- statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
- true -> {
- }
-
- else -> {}
- }
- }
- if (state.status == TunnelState.UP && state.tunnelConfig?.name != tunnelName) {
- tunnelName = state.tunnelConfig?.name
- launchVpnNotification(
- getString(R.string.tunnel_start_title),
- "${getString(R.string.tunnel_start_text)} - $tunnelName",
- )
- }
- }
- }
- }
-
- private fun launchAlwaysOnDisabledNotification() {
- launchVpnNotification(
- title = this.getString(R.string.vpn_connection_failed),
- description = this.getString(R.string.always_on_disabled),
- )
- }
-
- override fun stopService() {
- super.stopService()
- lifecycleScope.launch {
- vpnService.stopTunnel()
- didShowConnected = false
- }
- cancelJob()
- stopSelf()
- }
-
- private fun launchVpnNotification(title: String = getString(R.string.vpn_starting), description: String = getString(R.string.attempt_connection)) {
- val notification =
- notificationService.createNotification(
- channelId = getString(R.string.vpn_channel_id),
- channelName = getString(R.string.vpn_channel_name),
- title = title,
- onGoing = false,
- vibration = false,
- showTimestamp = true,
- description = description,
- )
- ServiceCompat.startForeground(
- this,
- foregroundId,
- notification,
- Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
- )
- }
-
- private fun launchVpnConnectionFailedNotification(message: String) {
- val notification =
- notificationService.createNotification(
- channelId = getString(R.string.vpn_channel_id),
- channelName = getString(R.string.vpn_channel_name),
- action =
- PendingIntent.getBroadcast(
- this,
- 0,
- Intent(this, NotificationActionReceiver::class.java),
- PendingIntent.FLAG_IMMUTABLE,
- ),
- actionText = getString(R.string.restart),
- title = getString(R.string.vpn_connection_failed),
- onGoing = false,
- vibration = true,
- showTimestamp = true,
- description = message,
- )
- ServiceCompat.startForeground(
- this,
- foregroundId,
- notification,
- Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
- )
- }
-
- private fun cancelJob() {
- try {
- job?.cancel()
- } catch (e: CancellationException) {
- Timber.i("Tunnel job cancelled")
- }
- }
-}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt
index 114bcfe..d75c219 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt
@@ -5,13 +5,14 @@ import androidx.activity.ComponentActivity
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
-import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
-import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
-import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
+import com.zaneschepke.wireguardautotunnel.service.foreground.AutoTunnelService
+import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
+import timber.log.Timber
import javax.inject.Inject
+import javax.inject.Provider
@AndroidEntryPoint
class ShortcutsActivity : ComponentActivity() {
@@ -19,7 +20,7 @@ class ShortcutsActivity : ComponentActivity() {
lateinit var appDataRepository: AppDataRepository
@Inject
- lateinit var serviceManager: ServiceManager
+ lateinit var tunnelService: Provider
@Inject
@ApplicationScope
@@ -31,31 +32,23 @@ class ShortcutsActivity : ComponentActivity() {
val settings = appDataRepository.settings.getSettings()
if (settings.isShortcutsEnabled) {
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
- WireGuardTunnelService::class.java.simpleName -> {
+ LEGACY_TUNNEL_SERVICE_NAME, TunnelService::class.java.simpleName -> {
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
- val tunnelConfig =
- tunnelName?.let {
- appDataRepository.tunnels.getAll().firstOrNull {
- it.name == tunnelName
- }
+ Timber.d("Tunnel name extra: $tunnelName")
+ val tunnelConfig = tunnelName?.let {
+ appDataRepository.tunnels.getAll()
+ .firstOrNull { it.name == tunnelName }
+ } ?: appDataRepository.getStartTunnelConfig()
+ Timber.d("Shortcut action on name: ${tunnelConfig?.name}")
+ tunnelConfig?.let {
+ when (intent.action) {
+ Action.START.name -> tunnelService.get().startTunnel(it)
+ Action.STOP.name -> tunnelService.get().stopTunnel(it)
+ else -> Unit
}
- when (intent.action) {
- Action.START.name ->
- serviceManager.startVpnServiceForeground(
- this@ShortcutsActivity,
- tunnelConfig?.id,
- isManualStart = true,
- )
-
- Action.STOP.name ->
- serviceManager.stopVpnServiceForeground(
- this@ShortcutsActivity,
- isManualStop = true,
- )
}
}
-
- WireGuardConnectivityWatcherService::class.java.simpleName -> {
+ AutoTunnelService::class.java.simpleName, LEGACY_AUTO_TUNNEL_SERVICE_NAME -> {
when (intent.action) {
Action.START.name ->
appDataRepository.settings.save(
@@ -63,7 +56,6 @@ class ShortcutsActivity : ComponentActivity() {
isAutoTunnelPaused = false,
),
)
-
Action.STOP.name ->
appDataRepository.settings.save(
settings.copy(
@@ -79,6 +71,8 @@ class ShortcutsActivity : ComponentActivity() {
}
companion object {
+ const val LEGACY_TUNNEL_SERVICE_NAME = "WireGuardTunnelService"
+ const val LEGACY_AUTO_TUNNEL_SERVICE_NAME = "WireGuardConnectivityWatcherService"
const val TUNNEL_NAME_EXTRA_KEY = "tunnelName"
const val CLASS_NAME_EXTRA_KEY = "className"
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/AutoTunnelControlTile.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/AutoTunnelControlTile.kt
index 7da23a6..8036765 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/AutoTunnelControlTile.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/AutoTunnelControlTile.kt
@@ -1,18 +1,20 @@
package com.zaneschepke.wireguardautotunnel.service.tile
+import android.content.Intent
import android.os.Build
+import android.os.IBinder
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.ServiceLifecycleDispatcher
+import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.R
-import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
+import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.cancel
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@@ -25,80 +27,123 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
@Inject
lateinit var serviceManager: ServiceManager
- private val dispatcher = ServiceLifecycleDispatcher(this)
+ @Inject
+ @ApplicationScope
+ lateinit var applicationScope: CoroutineScope
- private var manualStartConfig: TunnelConfig? = null
+ private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
- override fun onStartListening() {
- super.onStartListening()
- lifecycleScope.launch {
- val settings = appDataRepository.settings.getSettings()
- when (settings.isAutoTunnelEnabled) {
- true -> {
- if (settings.isAutoTunnelPaused) {
- setInactive()
- setTileDescription(this@AutoTunnelControlTile.getString(R.string.paused))
- } else {
- setActive()
- setTileDescription(this@AutoTunnelControlTile.getString(R.string.active))
+ /* This works around an annoying unsolved frameworks bug some people are hitting. */
+ override fun onBind(intent: Intent): IBinder? {
+ var ret: IBinder? = null
+ try {
+ ret = super.onBind(intent)
+ } catch (e: Throwable) {
+ Timber.e("Failed to bind to AutoTunnelTile")
+ }
+ return ret
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
+
+ applicationScope.launch {
+ appDataRepository.settings.getSettingsFlow().collect {
+ kotlin.runCatching {
+ when (it.isAutoTunnelEnabled) {
+ true -> {
+ if (it.isAutoTunnelPaused) {
+ setInactive()
+ setTileDescription(this@AutoTunnelControlTile.getString(R.string.paused))
+ } else {
+ setActive()
+ setTileDescription(this@AutoTunnelControlTile.getString(R.string.active))
+ }
+ }
+
+ false -> {
+ setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled))
+ setUnavailable()
+ }
}
- }
-
- false -> {
- setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled))
- setUnavailable()
+ }.onFailure {
+ Timber.e(it)
}
}
}
}
- override fun onTileAdded() {
- super.onTileAdded()
- onStartListening()
+ override fun onStopListening() {
+ lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
+ }
+
+ override fun onStartListening() {
+ super.onStartListening()
+ lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
}
override fun onClick() {
super.onClick()
unlockAndRun {
lifecycleScope.launch {
- try {
- appDataRepository.toggleWatcherServicePause()
- onStartListening()
- } catch (e: Exception) {
- Timber.e(e.message)
- } finally {
- cancel()
+ kotlin.runCatching {
+ val settings = appDataRepository.settings.getSettings()
+ if (settings.isAutoTunnelPaused) {
+ return@launch appDataRepository.settings.save(
+ settings.copy(
+ isAutoTunnelPaused = false,
+ ),
+ )
+ }
+ appDataRepository.settings.save(
+ settings.copy(
+ isAutoTunnelPaused = true,
+ ),
+ )
}
}
}
}
private fun setActive() {
- qsTile.state = Tile.STATE_ACTIVE
- qsTile.updateTile()
+ kotlin.runCatching {
+ qsTile.state = Tile.STATE_ACTIVE
+ qsTile.updateTile()
+ }
}
private fun setInactive() {
- qsTile.state = Tile.STATE_INACTIVE
- qsTile.updateTile()
+ kotlin.runCatching {
+ qsTile.state = Tile.STATE_INACTIVE
+ qsTile.updateTile()
+ }
}
private fun setUnavailable() {
- manualStartConfig = null
- qsTile.state = Tile.STATE_UNAVAILABLE
- qsTile.updateTile()
+ kotlin.runCatching {
+ qsTile.state = Tile.STATE_UNAVAILABLE
+ qsTile.updateTile()
+ }
}
private fun setTileDescription(description: String) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- qsTile.subtitle = description
+ kotlin.runCatching {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ qsTile.subtitle = description
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ qsTile.stateDescription = description
+ }
+ qsTile.updateTile()
}
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- qsTile.stateDescription = description
- }
- qsTile.updateTile()
}
override val lifecycle: Lifecycle
- get() = dispatcher.lifecycle
+ get() = lifecycleRegistry
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt
index d6e3817..cbbb85b 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt
@@ -5,18 +5,20 @@ import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.ServiceLifecycleDispatcher
+import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
-import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
-import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
-import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
+import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
+import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
+import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
+import com.zaneschepke.wireguardautotunnel.util.extensions.stopTunnelBackground
import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.cancel
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
+import javax.inject.Provider
@AndroidEntryPoint
class TunnelControlTile : TileService(), LifecycleOwner {
@@ -24,98 +26,103 @@ class TunnelControlTile : TileService(), LifecycleOwner {
lateinit var appDataRepository: AppDataRepository
@Inject
- lateinit var vpnService: VpnService
+ lateinit var tunnelService: Provider
@Inject
- lateinit var serviceManager: ServiceManager
+ @ApplicationScope
+ lateinit var applicationScope: CoroutineScope
- private val dispatcher = ServiceLifecycleDispatcher(this)
+ private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
- private var manualStartConfig: TunnelConfig? = null
+ override fun onCreate() {
+ super.onCreate()
+ Timber.d("onCreate for tile service")
+ lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
+ }
+
+ override fun onStopListening() {
+ lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
+ }
override fun onStartListening() {
super.onStartListening()
- Timber.d("On start listening called")
+ lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
lifecycleScope.launch {
- when (vpnService.getState()) {
- TunnelState.UP -> {
- setActive()
- setTileDescription(vpnService.name)
- }
-
- TunnelState.DOWN -> {
- setInactive()
- val config =
- appDataRepository.getStartTunnelConfig()?.also { config ->
- manualStartConfig = config
- } ?: appDataRepository.getPrimaryOrFirstTunnel()
- config?.let {
- setTileDescription(it.name)
- } ?: setUnavailable()
- }
-
- else -> setInactive()
- }
+ if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
+ updateTileState()
}
}
- override fun onTileAdded() {
- super.onTileAdded()
- onStartListening()
+ private suspend fun updateTileState() {
+ val lastActive = appDataRepository.getStartTunnelConfig()
+ lastActive?.let {
+ updateTile(it)
+ }
}
override fun onClick() {
super.onClick()
unlockAndRun {
lifecycleScope.launch {
- try {
- if (vpnService.getState() == TunnelState.UP) {
- serviceManager.stopVpnServiceForeground(
- this@TunnelControlTile,
- isManualStop = true,
- )
- } else {
- serviceManager.startVpnServiceForeground(
- this@TunnelControlTile,
- manualStartConfig?.id,
- isManualStart = true,
- )
- }
- } catch (e: Exception) {
- Timber.e(e.message)
- } finally {
- cancel()
+ val context = this@TunnelControlTile
+ val lastActive = appDataRepository.getStartTunnelConfig()
+ lastActive?.let { tunnel ->
+ if (tunnel.isActive) return@launch context.stopTunnelBackground(tunnel.id)
+ context.startTunnelBackground(tunnel.id)
}
}
}
}
private fun setActive() {
- qsTile.state = Tile.STATE_ACTIVE
- qsTile.updateTile()
+ kotlin.runCatching {
+ qsTile.state = Tile.STATE_ACTIVE
+ qsTile.updateTile()
+ }
}
private fun setInactive() {
- qsTile.state = Tile.STATE_INACTIVE
- qsTile.updateTile()
+ kotlin.runCatching {
+ qsTile.state = Tile.STATE_INACTIVE
+ qsTile.updateTile()
+ }
}
private fun setUnavailable() {
- manualStartConfig = null
- qsTile.state = Tile.STATE_UNAVAILABLE
- qsTile.updateTile()
+ kotlin.runCatching {
+ qsTile.state = Tile.STATE_UNAVAILABLE
+ setTileDescription("")
+ qsTile.updateTile()
+ }
}
private fun setTileDescription(description: String) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- qsTile.subtitle = description
+ kotlin.runCatching {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ qsTile.subtitle = description
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ qsTile.stateDescription = description
+ }
+ qsTile.updateTile()
}
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- qsTile.stateDescription = description
+ }
+
+ private fun updateTile(tunnelConfig: TunnelConfig?) {
+ kotlin.runCatching {
+ tunnelConfig?.let {
+ setTileDescription(it.name)
+ if (it.isActive) return setActive()
+ setInactive()
+ }
}
- qsTile.updateTile()
}
override val lifecycle: Lifecycle
- get() = dispatcher.lifecycle
+ get() = lifecycleRegistry
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/AlwaysOnVpnService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/AlwaysOnVpnService.kt
new file mode 100644
index 0000000..e65c294
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/AlwaysOnVpnService.kt
@@ -0,0 +1,46 @@
+package com.zaneschepke.wireguardautotunnel.service.tunnel
+
+import android.content.Intent
+import android.os.IBinder
+import androidx.lifecycle.LifecycleService
+import androidx.lifecycle.lifecycleScope
+import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.launch
+import timber.log.Timber
+import javax.inject.Inject
+import javax.inject.Provider
+
+@AndroidEntryPoint
+class AlwaysOnVpnService : LifecycleService() {
+
+ @Inject
+ lateinit var tunnelService: Provider
+
+ @Inject
+ lateinit var appDataRepository: AppDataRepository
+
+ override fun onBind(intent: Intent): IBinder? {
+ super.onBind(intent)
+ // We don't provide binding, so return null
+ return null
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ if (intent == null || intent.component == null || intent.component!!.packageName != packageName) {
+ Timber.i("Always-on VPN requested started")
+ lifecycleScope.launch {
+ val settings = appDataRepository.settings.getSettings()
+ if (settings.isAlwaysOnVpnEnabled) {
+ val tunnel = appDataRepository.getPrimaryOrFirstTunnel()
+ tunnel?.let {
+ tunnelService.get().startTunnel(it)
+ }
+ } else {
+ Timber.w("Always-on VPN is not enabled in app settings")
+ }
+ }
+ }
+ return super.onStartCommand(intent, flags, startId)
+ }
+}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/TunnelService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/TunnelService.kt
new file mode 100644
index 0000000..c89193c
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/TunnelService.kt
@@ -0,0 +1,17 @@
+package com.zaneschepke.wireguardautotunnel.service.tunnel
+
+import com.wireguard.android.backend.Tunnel
+import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
+import kotlinx.coroutines.flow.StateFlow
+
+interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel {
+ suspend fun startTunnel(tunnelConfig: TunnelConfig): Result
+
+ suspend fun stopTunnel(tunnelConfig: TunnelConfig): Result
+
+ val vpnState: StateFlow
+
+ suspend fun runningTunnelNames(): Set
+
+ suspend fun getState(): TunnelState
+}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnService.kt
deleted file mode 100644
index 311cbf6..0000000
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnService.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.zaneschepke.wireguardautotunnel.service.tunnel
-
-import com.wireguard.android.backend.Tunnel
-import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
-import kotlinx.coroutines.flow.StateFlow
-
-interface VpnService : Tunnel, org.amnezia.awg.backend.Tunnel {
- suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): TunnelState
-
- suspend fun stopTunnel()
-
- val vpnState: StateFlow
-
- fun getState(): TunnelState
-}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt
index 0046c22..da102fa 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt
@@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Backend
-import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Tunnel.State
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
@@ -14,6 +13,7 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStat
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics
import com.zaneschepke.wireguardautotunnel.util.Constants
+import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
@@ -32,103 +32,74 @@ import javax.inject.Provider
class WireGuardTunnel
@Inject
constructor(
- private val userspaceAmneziaBackend: Provider,
+ private val amneziaBackend: Provider,
@Userspace private val userspaceBackend: Provider,
@Kernel private val kernelBackend: Provider,
private val appDataRepository: AppDataRepository,
@ApplicationScope private val applicationScope: CoroutineScope,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
-) : VpnService {
+) : TunnelService {
private val _vpnState = MutableStateFlow(VpnState())
override val vpnState: StateFlow = _vpnState.asStateFlow()
+ override suspend fun runningTunnelNames(): Set {
+ return when (val backend = backend()) {
+ is Backend -> backend.runningTunnelNames
+ is org.amnezia.awg.backend.Backend -> backend.runningTunnelNames
+ else -> emptySet()
+ }
+ }
+
private var statsJob: Job? = null
- private var backendIsWgUserspace = true
-
- private var backendIsAmneziaUserspace = false
-
- init {
- applicationScope.launch(ioDispatcher) {
- appDataRepository.settings.getSettingsFlow().collect {
- if (it.isKernelEnabled && (backendIsWgUserspace || backendIsAmneziaUserspace)) {
- Timber.i("Setting kernel backend")
- backendIsWgUserspace = false
- backendIsAmneziaUserspace = false
- } else if (!it.isKernelEnabled && !it.isAmneziaEnabled && !backendIsWgUserspace) {
- Timber.i("Setting WireGuard userspace backend")
- backendIsWgUserspace = true
- backendIsAmneziaUserspace = false
- } else if (it.isAmneziaEnabled && !backendIsAmneziaUserspace) {
- Timber.i("Setting Amnezia userspace backend")
- backendIsAmneziaUserspace = true
- backendIsWgUserspace = false
+ private suspend fun setState(tunnelConfig: TunnelConfig, tunnelState: TunnelState): Result {
+ return runCatching {
+ when (val backend = backend()) {
+ is Backend -> backend.setState(this, tunnelState.toWgState(), TunnelConfig.configFromWgQuick(tunnelConfig.wgQuick)).let { TunnelState.from(it) }
+ is org.amnezia.awg.backend.Backend -> backend.setState(this, tunnelState.toAmState(), TunnelConfig.configFromAmQuick(tunnelConfig.amQuick)).let {
+ TunnelState.from(it)
}
+ else -> throw NotImplementedError()
}
+ }.onFailure {
+ Timber.e(it)
}
}
- private fun setState(tunnelConfig: TunnelConfig?, tunnelState: TunnelState): TunnelState {
- return if (backendIsAmneziaUserspace) {
- Timber.i("Using Amnezia backend")
- val config =
- tunnelConfig?.let {
- if (it.amQuick != "") {
- TunnelConfig.configFromAmQuick(it.amQuick)
- } else {
- Timber.w(
- "Using backwards compatible wg config, amnezia specific config not found.",
- )
- TunnelConfig.configFromAmQuick(it.wgQuick)
- }
- }
- val state =
- userspaceAmneziaBackend.get().setState(this, tunnelState.toAmState(), config)
- TunnelState.from(state)
- } else {
- Timber.i("Using Wg backend")
- val wgConfig = tunnelConfig?.let { TunnelConfig.configFromWgQuick(it.wgQuick) }
- val state =
- backend().setState(
- this,
- tunnelState.toWgState(),
- wgConfig,
- )
- TunnelState.from(state)
- }
+ private suspend fun backend(): Any {
+ val settings = appDataRepository.settings.getSettings()
+ if (settings.isKernelEnabled) return kernelBackend.get()
+ if (settings.isAmneziaEnabled) return amneziaBackend.get()
+ return userspaceBackend.get()
}
- override suspend fun startTunnel(tunnelConfig: TunnelConfig?): TunnelState {
+ override suspend fun startTunnel(tunnelConfig: TunnelConfig): Result {
return withContext(ioDispatcher) {
- try {
- // TODO we need better error handling here
- // need to bubble up these errors to the UI
- val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel()
- if (config != null) {
- emitTunnelConfig(config)
- setState(config, TunnelState.UP)
- } else {
- throw Exception("No tunnels")
- }
- } catch (e: BackendException) {
- Timber.e("Failed to start tunnel with error: ${e.message}")
- TunnelState.from(State.DOWN)
+ if (_vpnState.value.status == TunnelState.UP) vpnState.value.tunnelConfig?.let { stopTunnel(it) }
+ appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
+ appDataRepository.appState.setLastActiveTunnelId(tunnelConfig.id)
+ emitTunnelConfig(tunnelConfig)
+ setState(tunnelConfig, TunnelState.UP).onSuccess {
+ emitTunnelState(it)
+ WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
+ }.onFailure {
+ appDataRepository.tunnels.save(tunnelConfig.copy(isActive = false))
+ WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
}
}
}
- private fun backend(): Backend {
- return when {
- backendIsWgUserspace -> {
- userspaceBackend.get()
- }
-
- !backendIsWgUserspace && !backendIsAmneziaUserspace -> {
- kernelBackend.get()
- }
-
- else -> {
- userspaceBackend.get()
+ override suspend fun stopTunnel(tunnelConfig: TunnelConfig): Result {
+ return withContext(ioDispatcher) {
+ appDataRepository.tunnels.save(tunnelConfig.copy(isActive = false))
+ setState(tunnelConfig, TunnelState.DOWN).onSuccess {
+ emitTunnelState(it)
+ resetBackendStatistics()
+ WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
+ }.onFailure {
+ Timber.e(it)
+ appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
+ WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
}
}
}
@@ -149,41 +120,27 @@ constructor(
)
}
- private suspend fun emitTunnelConfig(tunnelConfig: TunnelConfig?) {
- _vpnState.emit(
+ private fun emitTunnelConfig(tunnelConfig: TunnelConfig?) {
+ _vpnState.tryEmit(
_vpnState.value.copy(
tunnelConfig = tunnelConfig,
),
)
}
- private fun resetVpnState() {
- _vpnState.tryEmit(VpnState())
+ private fun resetBackendStatistics() {
+ _vpnState.tryEmit(
+ _vpnState.value.copy(
+ statistics = null,
+ ),
+ )
}
- override suspend fun stopTunnel() {
- withContext(ioDispatcher) {
- try {
- if (getState() == TunnelState.UP) {
- val state = setState(null, TunnelState.DOWN)
- resetVpnState()
- emitTunnelState(state)
- }
- } catch (e: BackendException) {
- Timber.e("Failed to stop wireguard tunnel with error: ${e.message}")
- } catch (e: org.amnezia.awg.backend.BackendException) {
- Timber.e("Failed to stop amnezia tunnel with error: ${e.message}")
- }
- }
- }
-
- override fun getState(): TunnelState {
- return if (backendIsAmneziaUserspace) {
- TunnelState.from(
- userspaceAmneziaBackend.get().getState(this),
- )
- } else {
- TunnelState.from(backend().getState(this))
+ override suspend fun getState(): TunnelState {
+ return when (val backend = backend()) {
+ is Backend -> backend.getState(this).let { TunnelState.from(it) }
+ is org.amnezia.awg.backend.Backend -> backend.getState(this).let { TunnelState.from(it) }
+ else -> TunnelState.DOWN
}
}
@@ -197,7 +154,7 @@ constructor(
private fun handleStateChange(state: TunnelState) {
emitTunnelState(state)
- WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
+ WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
if (state == TunnelState.UP) {
statsJob = startTunnelStatisticsJob()
}
@@ -211,17 +168,19 @@ constructor(
}
private fun startTunnelStatisticsJob() = applicationScope.launch(ioDispatcher) {
+ val backend = backend()
while (true) {
- if (backendIsAmneziaUserspace) {
- emitBackendStatistics(
- AmneziaStatistics(
- userspaceAmneziaBackend.get().getStatistics(this@WireGuardTunnel),
- ),
- )
- } else {
- emitBackendStatistics(
- WireGuardStatistics(backend().getStatistics(this@WireGuardTunnel)),
+ when (backend) {
+ is Backend -> emitBackendStatistics(
+ WireGuardStatistics(backend.getStatistics(this@WireGuardTunnel)),
)
+ is org.amnezia.awg.backend.Backend -> {
+ emitBackendStatistics(
+ AmneziaStatistics(
+ backend.getStatistics(this@WireGuardTunnel),
+ ),
+ )
+ }
}
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppUiState.kt
index 7da35d6..281f346 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppUiState.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppUiState.kt
@@ -3,7 +3,6 @@ package com.zaneschepke.wireguardautotunnel.ui
data class AppUiState(
val snackbarMessage: String = "",
val snackbarMessageConsumed: Boolean = true,
- val vpnPermissionAccepted: Boolean = false,
val notificationPermissionAccepted: Boolean = false,
val requestPermissions: Boolean = false,
)
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt
index d760d9e..620ad54 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt
@@ -1,100 +1,29 @@
package com.zaneschepke.wireguardautotunnel.ui
-import android.content.ActivityNotFoundException
-import android.content.Context
-import android.content.Intent
-import android.net.Uri
import androidx.lifecycle.ViewModel
-import com.wireguard.android.backend.GoBackend
-import com.zaneschepke.wireguardautotunnel.R
-import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
-import com.zaneschepke.wireguardautotunnel.util.Constants
+import androidx.lifecycle.viewModelScope
+import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
-import timber.log.Timber
+import kotlinx.coroutines.launch
+import xyz.teamgravity.pin_lock_compose.PinManager
import javax.inject.Inject
@HiltViewModel
class AppViewModel
@Inject
-constructor() : ViewModel() {
- val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance)
+constructor(
+ private val appDataRepository: AppDataRepository,
+) : ViewModel() {
private val _appUiState =
MutableStateFlow(
- AppUiState(
- vpnPermissionAccepted = vpnIntent == null,
- ),
+ AppUiState(),
)
val appUiState = _appUiState.asStateFlow()
- fun isRequiredPermissionGranted(): Boolean {
- val allAccepted =
- (_appUiState.value.vpnPermissionAccepted && _appUiState.value.vpnPermissionAccepted)
- if (!allAccepted) requestPermissions()
- return allAccepted
- }
-
- private fun requestPermissions() {
- _appUiState.update {
- it.copy(
- requestPermissions = true,
- )
- }
- }
-
- fun permissionsRequested() {
- _appUiState.update {
- it.copy(
- requestPermissions = false,
- )
- }
- }
-
- fun openWebPage(url: String, context: Context) {
- try {
- val webpage: Uri = Uri.parse(url)
- val intent =
- Intent(Intent.ACTION_VIEW, webpage).apply {
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- }
- context.startActivity(intent)
- } catch (e: ActivityNotFoundException) {
- Timber.e(e)
- showSnackbarMessage(context.getString(R.string.no_browser_detected))
- }
- }
-
- fun onVpnPermissionAccepted() {
- _appUiState.update {
- it.copy(
- vpnPermissionAccepted = true,
- )
- }
- }
-
- fun launchEmail(context: Context) {
- try {
- val intent =
- Intent(Intent.ACTION_SENDTO).apply {
- type = Constants.EMAIL_MIME_TYPE
- putExtra(Intent.EXTRA_EMAIL, arrayOf(context.getString(R.string.my_email)))
- putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- }
- context.startActivity(
- Intent.createChooser(intent, context.getString(R.string.email_chooser)).apply {
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- },
- )
- } catch (e: ActivityNotFoundException) {
- Timber.e(e)
- showSnackbarMessage(context.getString(R.string.no_email_detected))
- }
- }
-
fun showSnackbarMessage(message: String) {
_appUiState.update {
it.copy(
@@ -113,11 +42,12 @@ constructor() : ViewModel() {
}
}
- fun setNotificationPermissionAccepted(accepted: Boolean) {
- _appUiState.update {
- it.copy(
- notificationPermissionAccepted = accepted,
- )
- }
+ fun onPinLockDisabled() = viewModelScope.launch {
+ PinManager.clearPin()
+ appDataRepository.appState.setPinLockEnabled(false)
+ }
+
+ fun onPinLockEnabled() = viewModelScope.launch {
+ appDataRepository.appState.setPinLockEnabled(true)
}
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt
index 1beeb5e..f99a8d0 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt
@@ -1,17 +1,11 @@
package com.zaneschepke.wireguardautotunnel.ui
-import android.Manifest
-import android.os.Build
import android.os.Bundle
import androidx.activity.SystemBarStyle
-import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
-import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
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.MaterialTheme
@@ -21,19 +15,15 @@ import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
-import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
-import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -44,16 +34,9 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
-import com.google.accompanist.permissions.ExperimentalPermissionsApi
-import com.google.accompanist.permissions.isGranted
-import com.google.accompanist.permissions.rememberPermissionState
-import com.google.accompanist.permissions.shouldShowRationale
-import com.zaneschepke.wireguardautotunnel.R
-import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
-import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
@@ -82,9 +65,6 @@ class MainActivity : AppCompatActivity() {
@Inject
lateinit var serviceManager: ServiceManager
- @OptIn(
- ExperimentalPermissionsApi::class,
- )
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -93,7 +73,6 @@ class MainActivity : AppCompatActivity() {
enableEdgeToEdge(navigationBarStyle = SystemBarStyle.dark(Color.Transparent.toArgb()))
lifecycleScope.launch {
- WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
val settings = settingsRepository.getSettings()
if (settings.isAutoTunnelEnabled) {
serviceManager.startWatcherService(application.applicationContext)
@@ -105,30 +84,9 @@ class MainActivity : AppCompatActivity() {
val appUiState by appViewModel.appUiState.collectAsStateWithLifecycle()
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
- var showVpnPermissionDialog by remember { mutableStateOf(false) }
-
- val notificationPermissionState =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
- } else {
- null
- }
val snackbarHostState = remember { SnackbarHostState() }
- val vpnActivityResultState =
- rememberLauncherForActivityResult(
- ActivityResultContracts.StartActivityForResult(),
- onResult = {
- val accepted = (it.resultCode == RESULT_OK)
- if (accepted) {
- appViewModel.onVpnPermissionAccepted()
- } else {
- showVpnPermissionDialog = true
- }
- },
- )
-
fun showSnackBarMessage(message: StringValue) {
lifecycleScope.launch(Dispatchers.Main) {
val result =
@@ -146,37 +104,7 @@ class MainActivity : AppCompatActivity() {
}
}
- LaunchedEffect(appUiState.requestPermissions) {
- if (appUiState.requestPermissions) {
- appViewModel.permissionsRequested()
- if (notificationPermissionState != null && !notificationPermissionState.status.isGranted
- ) {
- notificationPermissionState.launchPermissionRequest()
- return@LaunchedEffect if (notificationPermissionState.status.shouldShowRationale || !notificationPermissionState.status.isGranted) {
- showSnackBarMessage(
- StringValue.StringResource(R.string.notification_permission_required),
- )
- } else {
- Unit
- }
- }
- if (!appUiState.vpnPermissionAccepted) {
- return@LaunchedEffect appViewModel.vpnIntent?.let {
- vpnActivityResultState.launch(
- it,
- )
- } ?: Unit
- }
- }
- }
-
WireguardAutoTunnelTheme {
- LaunchedEffect(Unit) {
- appViewModel.setNotificationPermissionAccepted(
- notificationPermissionState?.status?.isGranted ?: true,
- )
- }
-
LaunchedEffect(appUiState.snackbarMessageConsumed) {
if (!appUiState.snackbarMessageConsumed) {
showSnackBarMessage(StringValue.DynamicString(appUiState.snackbarMessage))
@@ -186,21 +114,6 @@ class MainActivity : AppCompatActivity() {
val focusRequester = remember { FocusRequester() }
- if (showVpnPermissionDialog) {
- InfoDialog(
- onDismiss = { showVpnPermissionDialog = false },
- onAttest = { showVpnPermissionDialog = false },
- title = { Text(text = stringResource(R.string.vpn_denied_dialog_title)) },
- body = {
- Column(verticalArrangement = Arrangement.spacedBy(15.dp)) {
- Text(text = stringResource(R.string.vpn_denied_dialog_message))
- Text(text = stringResource(R.string.vpn_denied_dialog_message2))
- }
- },
- confirmText = { Text(text = stringResource(R.string.okay)) },
- )
- }
-
Scaffold(
snackbarHost = {
SnackbarHost(snackbarHostState) { snackbarData: SnackbarData ->
@@ -266,7 +179,6 @@ class MainActivity : AppCompatActivity() {
) {
SupportScreen(
focusRequester = focusRequester,
- appViewModel = appViewModel,
navController = navController,
)
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/SplashActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/SplashActivity.kt
index 55b7ec1..a12c09f 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/SplashActivity.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/SplashActivity.kt
@@ -11,14 +11,22 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.zaneschepke.logcatter.LocalLogCollector
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
-import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel.Companion.isRunningOnAndroidTv
+import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
+import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
+import com.zaneschepke.wireguardautotunnel.util.Constants
+import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
+import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
+import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
+import timber.log.Timber
import xyz.teamgravity.pin_lock_compose.PinManager
import javax.inject.Inject
+import javax.inject.Provider
@SuppressLint("CustomSplashScreen")
@AndroidEntryPoint
@@ -26,6 +34,12 @@ class SplashActivity : ComponentActivity() {
@Inject
lateinit var appStateRepository: AppStateRepository
+ @Inject
+ lateinit var appDataRepository: AppDataRepository
+
+ @Inject
+ lateinit var tunnelService: Provider
+
@Inject
lateinit var localLogCollector: LocalLogCollector
@@ -41,7 +55,7 @@ class SplashActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
applicationScope.launch {
- if (!isRunningOnAndroidTv()) localLogCollector.start()
+ if (!this@SplashActivity.isRunningOnTv()) localLogCollector.start()
}
lifecycleScope.launch {
@@ -50,6 +64,21 @@ class SplashActivity : ComponentActivity() {
if (pinLockEnabled) {
PinManager.initialize(WireGuardAutoTunnel.instance)
}
+ // TODO eventually make this support multi-tunnel
+ Timber.d("Check for active tunnels")
+ val settings = appDataRepository.settings.getSettings()
+ if (settings.isKernelEnabled) {
+ // delay in case state change is underway while app is opened
+ delay(Constants.FOCUS_REQUEST_DELAY)
+ val activeTunnels = appDataRepository.tunnels.getActive()
+ Timber.d("Kernel mode enabled, seeing if we need to start a tunnel")
+ activeTunnels.firstOrNull()?.let {
+ Timber.d("Trying to start active kernel tunnel: ${it.name}")
+ tunnelService.get().startTunnel(it)
+ }
+ }
+ requestTunnelTileServiceStateUpdate()
+ requestAutoTunnelTileServiceUpdate()
val intent =
Intent(this@SplashActivity, MainActivity::class.java).apply {
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt
index 8acc2dd..d14c007 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt
@@ -22,7 +22,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
-import com.zaneschepke.wireguardautotunnel.util.toThreeDecimalPlaceString
+import com.zaneschepke.wireguardautotunnel.util.extensions.toThreeDecimalPlaceString
@OptIn(ExperimentalFoundationApi::class)
@Composable
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/functions/Functions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/functions/Functions.kt
new file mode 100644
index 0000000..59d8986
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/functions/Functions.kt
@@ -0,0 +1,53 @@
+package com.zaneschepke.wireguardautotunnel.ui.common.functions
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
+import androidx.activity.compose.ManagedActivityResultLauncher
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.runtime.Composable
+import com.zaneschepke.wireguardautotunnel.util.Constants
+
+@Composable
+fun rememberFileImportLauncherForResult(onNoFileExplorer: () -> Unit, onData: (data: Uri) -> Unit): ManagedActivityResultLauncher {
+ return 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)
+ }
+ ) {
+ onNoFileExplorer()
+ }
+ return intent
+ }
+ },
+ ) { data ->
+ if (data == null) return@rememberLauncherForActivityResult
+ onData(data)
+ }
+}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt
index 3baf74f..bac9b82 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt
@@ -13,6 +13,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.navigation.NavController
+import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.currentBackStackEntryAsState
import com.zaneschepke.wireguardautotunnel.ui.Screen
@@ -39,7 +40,19 @@ fun BottomNavBar(navController: NavController, bottomNavItems: List = arrayListOf(PeerProxy()),
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt
index 1e29adf..0d30050 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt
@@ -23,8 +23,8 @@ import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
-import com.zaneschepke.wireguardautotunnel.util.removeAt
-import com.zaneschepke.wireguardautotunnel.util.update
+import com.zaneschepke.wireguardautotunnel.util.extensions.removeAt
+import com.zaneschepke.wireguardautotunnel.util.extensions.update
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
@@ -142,7 +142,6 @@ constructor(
private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) = viewModelScope.launch {
if (tunnelConfig != null) {
saveConfig(tunnelConfig).join()
- WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
}
}
@@ -294,7 +293,7 @@ constructor(
fun onSaveAllChanges(configType: ConfigType): Result {
return try {
- val wgQuick = buildConfig().toWgQuickString()
+ val wgQuick = buildConfig().toWgQuickString(true)
val amQuick =
if (configType == ConfigType.AMNEZIA) {
buildAmConfig().toAwgQuickString()
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt
index ac356ed..ec90370 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt
@@ -1,40 +1,26 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main
import android.annotation.SuppressLint
-import android.content.Context
-import android.content.Intent
-import android.content.pm.PackageManager
-import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity.RESULT_OK
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
-import androidx.compose.animation.slideInVertically
-import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.focusable
import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
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.text.ClickableText
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Create
-import androidx.compose.material.icons.filled.FileOpen
-import androidx.compose.material.icons.filled.QrCode
import androidx.compose.material.icons.rounded.Bolt
import androidx.compose.material.icons.rounded.Circle
import androidx.compose.material.icons.rounded.CopyAll
@@ -43,22 +29,15 @@ import androidx.compose.material.icons.rounded.Info
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material.icons.rounded.Smartphone
import androidx.compose.material.icons.rounded.Star
-import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition
-import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
-import androidx.compose.material3.rememberModalBottomSheetState
-import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -79,23 +58,15 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
-import androidx.compose.ui.text.font.FontStyle
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
-import com.iamageo.multifablibrary.FabIcon
-import com.iamageo.multifablibrary.FabOption
-import com.iamageo.multifablibrary.MultiFabItem
-import com.iamageo.multifablibrary.MultiFloatingActionButton
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions
+import com.wireguard.android.backend.GoBackend
import com.zaneschepke.wireguardautotunnel.R
-import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
@@ -103,18 +74,27 @@ import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
+import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
+import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.GettingStartedLabel
+import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ScrollDismissMultiFab
+import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelImportSheet
+import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.VpnDeniedDialog
import com.zaneschepke.wireguardautotunnel.ui.theme.corn
import com.zaneschepke.wireguardautotunnel.ui.theme.mint
import com.zaneschepke.wireguardautotunnel.util.Constants
-import com.zaneschepke.wireguardautotunnel.util.getMessage
-import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
-import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
+import com.zaneschepke.wireguardautotunnel.util.extensions.getMessage
+import com.zaneschepke.wireguardautotunnel.util.extensions.handshakeStatus
+import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
+import com.zaneschepke.wireguardautotunnel.util.extensions.mapPeerStats
+import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
+import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
+import timber.log.Timber
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
-@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
+@OptIn(ExperimentalFoundationApi::class)
@Composable
fun MainScreen(
viewModel: MainViewModel = hiltViewModel(),
@@ -124,14 +104,16 @@ fun MainScreen(
) {
val haptic = LocalHapticFeedback.current
val context = LocalContext.current
- val isVisible = rememberSaveable { mutableStateOf(true) }
val scope = rememberCoroutineScope()
- val sheetState = rememberModalBottomSheetState()
var showBottomSheet by remember { mutableStateOf(false) }
var configType by remember { mutableStateOf(ConfigType.WIREGUARD) }
+ var showVpnPermissionDialog by remember { mutableStateOf(false) }
+ val isVisible = rememberSaveable { mutableStateOf(true) }
+ var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
+ var selectedTunnel by remember { mutableStateOf(null) }
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
- // Nested scroll for control FAB
val nestedScrollConnection =
remember {
object : NestedScrollConnection {
@@ -144,67 +126,43 @@ fun MainScreen(
if (available.y > 1) {
isVisible.value = true
}
-
return Offset.Zero
}
}
}
- var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
- var selectedTunnel by remember { mutableStateOf(null) }
- val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+ val vpnActivityResultState =
+ rememberLauncherForActivityResult(
+ ActivityResultContracts.StartActivityForResult(),
+ onResult = {
+ val accepted = (it.resultCode == RESULT_OK)
+ if (accepted) {
+ Timber.d("VPN permission granted")
+ } else {
+ showVpnPermissionDialog = true
+ }
+ },
+ )
LaunchedEffect(Unit) {
- if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
+ if (context.isRunningOnTv()) {
delay(Constants.FOCUS_REQUEST_DELAY)
focusRequester.requestFocus()
}
}
- 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)
- }
- ) {
- appViewModel.showSnackbarMessage(
- context.getString(R.string.error_no_file_explorer),
- )
- }
- return intent
- }
- },
- ) { data ->
- if (data == null) return@rememberLauncherForActivityResult
- scope.launch {
- viewModel.onTunnelFileSelected(data, configType, context).onFailure {
- appViewModel.showSnackbarMessage(it.getMessage(context))
- }
+ val tunnelFileImportResultLauncher = rememberFileImportLauncherForResult(onNoFileExplorer = {
+ appViewModel.showSnackbarMessage(
+ context.getString(R.string.error_no_file_explorer),
+ )
+ }, onData = { data ->
+ scope.launch {
+ viewModel.onTunnelFileSelected(data, configType, context).onFailure {
+ appViewModel.showSnackbarMessage(it.getMessage(context))
}
}
+ })
+
val scanLauncher =
rememberLauncherForActivityResult(
contract = ScanContract(),
@@ -219,6 +177,8 @@ fun MainScreen(
},
)
+ VpnDeniedDialog(showVpnPermissionDialog, onDismiss = { showVpnPermissionDialog = false })
+
if (showDeleteTunnelAlertDialog) {
InfoDialog(
onDismiss = { showDeleteTunnelAlertDialog = false },
@@ -234,14 +194,16 @@ fun MainScreen(
}
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
- if (appViewModel.isRequiredPermissionGranted()) {
- if (checked) {
- viewModel.onTunnelStart(tunnel, context)
+ if (checked) {
+ if (uiState.settings.isKernelEnabled) {
+ context.startTunnelBackground(tunnel.id)
} else {
- viewModel.onTunnelStop(
- context,
- )
+ viewModel.onTunnelStart(tunnel)
}
+ } else {
+ viewModel.onTunnelStop(
+ tunnel,
+ )
}
}
@@ -273,161 +235,23 @@ fun MainScreen(
},
floatingActionButtonPosition = FabPosition.End,
floatingActionButton = {
- AnimatedVisibility(
- visible = isVisible.value,
- enter = slideInVertically(initialOffsetY = { it * 2 }),
- exit = slideOutVertically(targetOffsetY = { it * 2 }),
- modifier =
- Modifier
- .focusRequester(focusRequester)
- .focusGroup(),
- ) {
- val secondaryColor = MaterialTheme.colorScheme.secondary
- val tvFobColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
- val fobColor =
- if (WireGuardAutoTunnel.isRunningOnAndroidTv()) tvFobColor else secondaryColor
- val fobIconColor =
- if (WireGuardAutoTunnel.isRunningOnAndroidTv()) Color.White else MaterialTheme.colorScheme.background
- MultiFloatingActionButton(
- fabIcon =
- FabIcon(
- iconRes = R.drawable.add,
- iconResAfterRotate = R.drawable.close,
- iconRotate = 180f,
- ),
- fabOption =
- FabOption(
- iconTint = fobIconColor,
- backgroundTint = fobColor,
- ),
- itemsMultiFab =
- listOf(
- MultiFabItem(
- label = {
- Text(
- stringResource(id = R.string.amnezia),
- color = Color.White,
- textAlign = TextAlign.Center,
- modifier = Modifier.padding(end = 10.dp),
- )
- },
- modifier =
- Modifier
- .size(40.dp),
- icon = R.drawable.add,
- value = ConfigType.AMNEZIA.name,
- miniFabOption =
- FabOption(
- backgroundTint = fobColor,
- fobIconColor,
- ),
- ),
- MultiFabItem(
- label = {
- Text(
- stringResource(id = R.string.wireguard),
- color = Color.White,
- textAlign = TextAlign.Center,
- modifier = Modifier.padding(end = 10.dp),
- )
- },
- icon = R.drawable.add,
- value = ConfigType.WIREGUARD.name,
- miniFabOption =
- FabOption(
- backgroundTint = fobColor,
- fobIconColor,
- ),
- ),
- ),
- onFabItemClicked = {
- showBottomSheet = true
- configType = ConfigType.valueOf(it.value)
- },
- shape = RoundedCornerShape(16.dp),
- )
- }
+ ScrollDismissMultiFab(R.drawable.add, focusRequester, isVisible = isVisible.value, onFabItemClicked = {
+ showBottomSheet = true
+ configType = ConfigType.valueOf(it.value)
+ })
},
) {
- if (showBottomSheet) {
- ModalBottomSheet(
- onDismissRequest = {
- showBottomSheet = false
- },
- sheetState = sheetState,
- ) {
- // Sheet content
- Row(
- modifier =
- Modifier
- .fillMaxWidth()
- .clickable {
- showBottomSheet = false
- tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
- }
- .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()) {
- HorizontalDivider()
- Row(
- modifier =
- Modifier
- .fillMaxWidth()
- .clickable {
- scope.launch {
- showBottomSheet = false
- launchQrScanner()
- }
- }
- .padding(10.dp),
- ) {
- Icon(
- Icons.Filled.QrCode,
- contentDescription = stringResource(id = R.string.qr_scan),
- modifier = Modifier.padding(10.dp),
- )
- Text(
- stringResource(id = R.string.add_from_qr),
- modifier = Modifier.padding(10.dp),
- )
- }
- }
- HorizontalDivider()
- Row(
- modifier =
- Modifier
- .fillMaxWidth()
- .clickable {
- showBottomSheet = false
- navController.navigate(
- "${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}?configType=$configType",
- )
- }
- .padding(10.dp),
- ) {
- Icon(
- Icons.Filled.Create,
- contentDescription = stringResource(id = R.string.create_import),
- modifier = Modifier.padding(10.dp),
- )
- Text(
- stringResource(id = R.string.create_import),
- modifier = Modifier.padding(10.dp),
- )
- }
- }
- }
-
+ TunnelImportSheet(
+ showBottomSheet,
+ onDismiss = { showBottomSheet = false },
+ onFileClick = { tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES) },
+ onQrClick = { launchQrScanner() },
+ onManualImportClick = {
+ navController.navigate(
+ "${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}?configType=$configType",
+ )
+ },
+ )
LazyColumn(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
@@ -447,53 +271,7 @@ fun MainScreen(
exit = fadeOut(),
enter = fadeIn(),
) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center,
- modifier =
- Modifier
- .padding(top = 100.dp)
- .fillMaxSize(),
- ) {
- val gettingStarted =
- buildAnnotatedString {
- append(stringResource(id = R.string.see_the))
- append(" ")
- pushStringAnnotation(
- tag = "gettingStarted",
- annotation = stringResource(id = R.string.getting_started_url),
- )
- withStyle(
- style = SpanStyle(color = MaterialTheme.colorScheme.primary),
- ) {
- append(stringResource(id = R.string.getting_started_guide))
- }
- pop()
- append(" ")
- append(stringResource(R.string.unsure_how))
- append(".")
- }
- Text(
- text = stringResource(R.string.no_tunnels),
- fontStyle = FontStyle.Italic,
- )
- ClickableText(
- modifier =
- Modifier
- .padding(vertical = 10.dp, horizontal = 24.dp),
- text = gettingStarted,
- style =
- MaterialTheme.typography.bodyMedium.copy(
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- textAlign = TextAlign.Center,
- ),
- ) {
- gettingStarted.getStringAnnotations(tag = "gettingStarted", it, it)
- .firstOrNull()?.let { annotation ->
- appViewModel.openWebPage(annotation.item, context)
- }
- }
- }
+ GettingStartedLabel(onClick = { context.openWebUrl(it) })
}
}
item {
@@ -550,7 +328,7 @@ fun MainScreen(
}
},
onClick = {
- if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
+ if (context.isRunningOnTv()) {
itemFocusRequester.requestFocus()
}
},
@@ -565,11 +343,11 @@ fun MainScreen(
uiState.tunnels,
key = { tunnel -> tunnel.id },
) { tunnel ->
+ val isActive = uiState.tunnels.any { it.id == tunnel.id && it.isActive }
val leadingIconColor =
(
if (
- uiState.vpnState.tunnelConfig?.name == tunnel.name &&
- uiState.vpnState.status == TunnelState.UP
+ isActive
) {
uiState.vpnState.statistics
?.mapPeerStats()
@@ -631,10 +409,9 @@ fun MainScreen(
selectedTunnel = tunnel
},
onClick = {
- if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
+ if (!context.isRunningOnTv()) {
if (
- uiState.vpnState.status == TunnelState.UP &&
- (uiState.vpnState.tunnelConfig?.name == tunnel.name)
+ isActive
) {
expanded.value = !expanded.value
}
@@ -649,7 +426,7 @@ fun MainScreen(
rowButton = {
if (
tunnel.id == selectedTunnel?.id &&
- !WireGuardAutoTunnel.isRunningOnAndroidTv()
+ !context.isRunningOnTv()
) {
Row {
IconButton(
@@ -690,26 +467,19 @@ fun MainScreen(
}
}
} else {
- val checked by remember {
- derivedStateOf {
- (
- uiState.vpnState.status == TunnelState.UP &&
- tunnel.name == uiState.vpnState.tunnelConfig?.name
- )
- }
- }
- if (!checked) expanded.value = false
-
+ if (!isActive) expanded.value = false
@Composable
fun TunnelSwitch() = Switch(
modifier = Modifier.focusRequester(itemFocusRequester),
- checked = checked,
+ checked = isActive,
onCheckedChange = { checked ->
if (!checked) expanded.value = false
+ val intent = if (uiState.settings.isKernelEnabled) null else GoBackend.VpnService.prepare(context)
+ if (intent != null) return@Switch vpnActivityResultState.launch(intent)
onTunnelToggle(checked, tunnel)
},
)
- if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
+ if (context.isRunningOnTv()) {
Row {
IconButton(
onClick = {
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainUiState.kt
index fe49c30..0de6edc 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainUiState.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainUiState.kt
@@ -2,7 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
-import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
+import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
data class MainUiState(
val settings: Settings = Settings(),
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt
index c4f0fbf..19c4880 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt
@@ -7,17 +7,16 @@ import android.provider.OpenableColumns
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wireguard.config.Config
-import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
-import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
+import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
-import com.zaneschepke.wireguardautotunnel.util.toWgQuickString
+import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.SharingStarted
@@ -36,14 +35,14 @@ class MainViewModel
constructor(
private val appDataRepository: AppDataRepository,
private val serviceManager: ServiceManager,
- val vpnService: VpnService,
+ val tunnelService: TunnelService,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ViewModel() {
val uiState =
combine(
appDataRepository.settings.getSettingsFlow(),
appDataRepository.tunnels.getTunnelConfigsFlow(),
- vpnService.vpnState,
+ tunnelService.vpnState,
) { settings, tunnels, vpnState ->
MainUiState(settings, tunnels, vpnState, false)
}
@@ -66,7 +65,6 @@ constructor(
resetTunnelSetting(settings)
}
appDataRepository.tunnels.delete(tunnel)
- WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
}
}
@@ -79,18 +77,14 @@ constructor(
)
}
- fun onTunnelStart(tunnelConfig: TunnelConfig, context: Context) = viewModelScope.launch {
- Timber.d("On start called!")
- serviceManager.startVpnService(
- context,
- tunnelConfig.id,
- isManualStart = true,
- )
+ fun onTunnelStart(tunnelConfig: TunnelConfig) = viewModelScope.launch {
+ Timber.i("Starting tunnel ${tunnelConfig.name}")
+ tunnelService.startTunnel(tunnelConfig)
}
- fun onTunnelStop(context: Context) = viewModelScope.launch {
+ fun onTunnelStop(tunnel: TunnelConfig) = viewModelScope.launch {
Timber.i("Stopping active tunnel")
- serviceManager.stopVpnService(context, isManualStop = true)
+ tunnelService.stopTunnel(tunnel)
}
private fun validateConfigString(config: String, configType: ConfigType) {
@@ -171,7 +165,7 @@ constructor(
var tunnelName = name
var num = 1
while (tunnels.any { it.name == tunnelName }) {
- tunnelName = name + "($num)"
+ tunnelName = "$name($num)"
num++
}
tunnelName
@@ -190,7 +184,7 @@ constructor(
}
ConfigType.WIREGUARD -> {
- Config.parse(it).toWgQuickString()
+ Config.parse(it).toWgQuickString(true)
}
}
}
@@ -263,7 +257,7 @@ constructor(
}
ConfigType.WIREGUARD -> {
- Config.parse(zip).toWgQuickString()
+ Config.parse(zip).toWgQuickString(true)
}
}
addTunnel(
@@ -301,23 +295,19 @@ constructor(
}
private fun addTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
- val firstTunnel = appDataRepository.tunnels.count() == 0
saveTunnel(tunnelConfig)
- if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
}
fun pauseAutoTunneling() = viewModelScope.launch {
appDataRepository.settings.save(
uiState.value.settings.copy(isAutoTunnelPaused = true),
)
- WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
}
fun resumeAutoTunneling() = viewModelScope.launch {
appDataRepository.settings.save(
uiState.value.settings.copy(isAutoTunnelPaused = false),
)
- WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
}
private fun saveTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/GettingStartedLabel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/GettingStartedLabel.kt
new file mode 100644
index 0000000..937239f
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/GettingStartedLabel.kt
@@ -0,0 +1,71 @@
+package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
+
+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.foundation.text.ClickableText
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.unit.dp
+import com.zaneschepke.wireguardautotunnel.R
+
+@Composable
+fun GettingStartedLabel(onClick: (url: String) -> Unit) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ modifier =
+ Modifier
+ .padding(top = 100.dp)
+ .fillMaxSize(),
+ ) {
+ val gettingStarted =
+ buildAnnotatedString {
+ append(stringResource(id = R.string.see_the))
+ append(" ")
+ pushStringAnnotation(
+ tag = "gettingStarted",
+ annotation = stringResource(id = R.string.getting_started_url),
+ )
+ withStyle(
+ style = SpanStyle(color = MaterialTheme.colorScheme.primary),
+ ) {
+ append(stringResource(id = R.string.getting_started_guide))
+ }
+ pop()
+ append(" ")
+ append(stringResource(R.string.unsure_how))
+ append(".")
+ }
+ Text(
+ text = stringResource(R.string.no_tunnels),
+ fontStyle = FontStyle.Italic,
+ )
+ ClickableText(
+ modifier =
+ Modifier
+ .padding(vertical = 10.dp, horizontal = 24.dp),
+ text = gettingStarted,
+ style =
+ MaterialTheme.typography.bodyMedium.copy(
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ textAlign = TextAlign.Center,
+ ),
+ ) {
+ gettingStarted.getStringAnnotations(tag = "gettingStarted", it, it)
+ .firstOrNull()?.let { annotation ->
+ onClick(annotation.item)
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/ScrollDismissMultiFab.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/ScrollDismissMultiFab.kt
new file mode 100644
index 0000000..6a1cbb0
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/ScrollDismissMultiFab.kt
@@ -0,0 +1,108 @@
+package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
+
+import androidx.annotation.DrawableRes
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.foundation.focusGroup
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import com.iamageo.multifablibrary.FabIcon
+import com.iamageo.multifablibrary.FabOption
+import com.iamageo.multifablibrary.MultiFabItem
+import com.iamageo.multifablibrary.MultiFloatingActionButton
+import com.zaneschepke.wireguardautotunnel.R
+import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
+
+@Composable
+fun ScrollDismissMultiFab(
+ @DrawableRes res: Int,
+ focusRequester: FocusRequester,
+ isVisible: Boolean,
+ onFabItemClicked: (fabItem: MultiFabItem) -> Unit,
+) {
+ // Nested scroll for control FAB
+
+ val context = LocalContext.current
+
+ AnimatedVisibility(
+ visible = isVisible,
+ enter = slideInVertically(initialOffsetY = { it * 2 }),
+ exit = slideOutVertically(targetOffsetY = { it * 2 }),
+ modifier =
+ Modifier
+ .focusRequester(focusRequester)
+ .focusGroup(),
+ ) {
+ val fobColor = MaterialTheme.colorScheme.secondary
+ val fobIconColor = MaterialTheme.colorScheme.background
+ MultiFloatingActionButton(
+ fabIcon =
+ FabIcon(
+ iconRes = res,
+ iconResAfterRotate = R.drawable.close,
+ iconRotate = 180f,
+ ),
+ fabOption =
+ FabOption(
+ iconTint = fobIconColor,
+ backgroundTint = fobColor,
+ ),
+ itemsMultiFab =
+ listOf(
+ MultiFabItem(
+ label = {
+ Text(
+ stringResource(id = R.string.amnezia),
+ color = MaterialTheme.colorScheme.onBackground,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.padding(end = 10.dp),
+ )
+ },
+ modifier =
+ Modifier
+ .size(40.dp),
+ icon = res,
+ value = ConfigType.AMNEZIA.name,
+ miniFabOption =
+ FabOption(
+ backgroundTint = fobColor,
+ fobIconColor,
+ ),
+ ),
+ MultiFabItem(
+ label = {
+ Text(
+ stringResource(id = R.string.wireguard),
+ color = MaterialTheme.colorScheme.onBackground,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.padding(end = 10.dp),
+ )
+ },
+ icon = res,
+ value = ConfigType.WIREGUARD.name,
+ miniFabOption =
+ FabOption(
+ backgroundTint = fobColor,
+ fobIconColor,
+ ),
+ ),
+ ),
+ onFabItemClicked = {
+ onFabItemClicked(it)
+ },
+ shape = RoundedCornerShape(16.dp),
+ )
+ }
+}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelImportSheet.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelImportSheet.kt
new file mode 100644
index 0000000..c0cf458
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelImportSheet.kt
@@ -0,0 +1,104 @@
+package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Create
+import androidx.compose.material.icons.filled.FileOpen
+import androidx.compose.material.icons.filled.QrCode
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.zaneschepke.wireguardautotunnel.R
+import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun TunnelImportSheet(show: Boolean, onDismiss: () -> Unit, onFileClick: () -> Unit, onQrClick: () -> Unit, onManualImportClick: () -> Unit) {
+ val sheetState = rememberModalBottomSheetState()
+
+ val context = LocalContext.current
+ if (show) {
+ ModalBottomSheet(
+ onDismissRequest = {
+ onDismiss()
+ },
+ sheetState = sheetState,
+ ) {
+ Row(
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .clickable {
+ onDismiss()
+ onFileClick()
+ }
+ .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 (!context.isRunningOnTv()) {
+ HorizontalDivider()
+ Row(
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .clickable {
+ onDismiss()
+ onQrClick()
+ }
+ .padding(10.dp),
+ ) {
+ Icon(
+ Icons.Filled.QrCode,
+ contentDescription = stringResource(id = R.string.qr_scan),
+ modifier = Modifier.padding(10.dp),
+ )
+ Text(
+ stringResource(id = R.string.add_from_qr),
+ modifier = Modifier.padding(10.dp),
+ )
+ }
+ }
+ HorizontalDivider()
+ Row(
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .clickable {
+ onDismiss()
+ onManualImportClick()
+ }
+ .padding(10.dp),
+ ) {
+ Icon(
+ Icons.Filled.Create,
+ contentDescription = stringResource(id = R.string.create_import),
+ modifier = Modifier.padding(10.dp),
+ )
+ Text(
+ stringResource(id = R.string.create_import),
+ modifier = Modifier.padding(10.dp),
+ )
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/VpnDeniedDialog.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/VpnDeniedDialog.kt
new file mode 100644
index 0000000..04dc9c7
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/VpnDeniedDialog.kt
@@ -0,0 +1,49 @@
+package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
+
+import androidx.compose.foundation.text.ClickableText
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.withStyle
+import com.zaneschepke.wireguardautotunnel.R
+import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
+import com.zaneschepke.wireguardautotunnel.util.extensions.launchVpnSettings
+
+@Composable
+fun VpnDeniedDialog(show: Boolean, onDismiss: () -> Unit) {
+ val context = LocalContext.current
+ if (show) {
+ val alwaysOnDescription = buildAnnotatedString {
+ append(stringResource(R.string.always_on_message))
+ append(" ")
+ pushStringAnnotation(tag = "vpnSettings", annotation = "")
+ withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) {
+ append(stringResource(id = R.string.vpn_settings))
+ }
+ pop()
+ append(" ")
+ append(stringResource(R.string.always_on_message2))
+ append(".")
+ }
+ InfoDialog(
+ onDismiss = { onDismiss() },
+ onAttest = { onDismiss() },
+ title = { Text(text = stringResource(R.string.vpn_denied_dialog_title)) },
+ body = {
+ ClickableText(
+ text = alwaysOnDescription,
+ style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.outline),
+ ) {
+ alwaysOnDescription.getStringAnnotations(tag = "vpnSettings", it, it).firstOrNull()?.let {
+ context.launchVpnSettings()
+ }
+ }
+ },
+ confirmText = { Text(text = stringResource(R.string.okay)) },
+ )
+ }
+}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt
index a0299fa..4f5861e 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt
@@ -1,11 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.options
import android.annotation.SuppressLint
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.slideInVertically
-import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -16,7 +12,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
@@ -32,7 +27,6 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
-import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -44,32 +38,27 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
-import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
-import com.iamageo.multifablibrary.FabIcon
-import com.iamageo.multifablibrary.FabOption
-import com.iamageo.multifablibrary.MultiFabItem
-import com.iamageo.multifablibrary.MultiFloatingActionButton
import com.zaneschepke.wireguardautotunnel.R
-import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
+import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ScrollDismissMultiFab
import com.zaneschepke.wireguardautotunnel.util.Constants
-import com.zaneschepke.wireguardautotunnel.util.getMessage
+import com.zaneschepke.wireguardautotunnel.util.extensions.getMessage
+import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -97,7 +86,7 @@ fun OptionsScreen(
LaunchedEffect(Unit) {
optionsViewModel.init(tunnelId)
- if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
+ if (context.isRunningOnTv()) {
delay(Constants.FOCUS_REQUEST_DELAY)
focusRequester.requestFocus()
}
@@ -117,82 +106,12 @@ fun OptionsScreen(
Scaffold(
floatingActionButton = {
- val secondaryColor = MaterialTheme.colorScheme.secondary
- val tvFobColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
- val fobColor =
- if (WireGuardAutoTunnel.isRunningOnAndroidTv()) tvFobColor else secondaryColor
- val fobIconColor =
- if (WireGuardAutoTunnel.isRunningOnAndroidTv()) Color.White else MaterialTheme.colorScheme.background
- AnimatedVisibility(
- visible = true,
- enter = slideInVertically(initialOffsetY = { it * 2 }),
- exit = slideOutVertically(targetOffsetY = { it * 2 }),
- modifier =
- Modifier
- .focusRequester(focusRequester)
- .focusGroup(),
- ) {
- MultiFloatingActionButton(
- fabIcon =
- FabIcon(
- iconRes = R.drawable.edit,
- iconResAfterRotate = R.drawable.close,
- iconRotate = 180f,
- ),
- fabOption =
- FabOption(
- iconTint = fobIconColor,
- backgroundTint = fobColor,
- ),
- itemsMultiFab =
- listOf(
- MultiFabItem(
- label = {
- Text(
- stringResource(id = R.string.amnezia),
- color = Color.White,
- textAlign = TextAlign.Center,
- modifier = Modifier.padding(end = 10.dp),
- )
- },
- modifier =
- Modifier
- .size(40.dp),
- icon = R.drawable.edit,
- value = ConfigType.AMNEZIA.name,
- miniFabOption =
- FabOption(
- backgroundTint = fobColor,
- fobIconColor,
- ),
- ),
- MultiFabItem(
- label = {
- Text(
- stringResource(id = R.string.wireguard),
- color = Color.White,
- textAlign = TextAlign.Center,
- modifier = Modifier.padding(end = 10.dp),
- )
- },
- icon = R.drawable.edit,
- value = ConfigType.WIREGUARD.name,
- miniFabOption =
- FabOption(
- backgroundTint = fobColor,
- fobIconColor,
- ),
- ),
- ),
- onFabItemClicked = {
- val configType = ConfigType.valueOf(it.value)
- navController.navigate(
- "${Screen.Config.route}/$tunnelId?configType=${configType.name}",
- )
- },
- shape = RoundedCornerShape(16.dp),
+ ScrollDismissMultiFab(R.drawable.edit, focusRequester, isVisible = true, onFabItemClicked = {
+ val configType = ConfigType.valueOf(it.value)
+ navController.navigate(
+ "${Screen.Config.route}/$tunnelId?configType=${configType.name}",
)
- }
+ })
},
) {
Column(
@@ -216,7 +135,7 @@ fun OptionsScreen(
color = MaterialTheme.colorScheme.surface,
modifier =
(
- if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
+ if (context.isRunningOnTv()) {
Modifier
.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth)
@@ -257,7 +176,7 @@ fun OptionsScreen(
color = MaterialTheme.colorScheme.surface,
modifier =
(
- if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
+ if (context.isRunningOnTv()) {
Modifier
.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth)
@@ -297,13 +216,13 @@ fun OptionsScreen(
uiState.tunnel?.tunnelNetworks?.forEach { ssid ->
ClickableIconButton(
onClick = {
- if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
+ if (context.isRunningOnTv()) {
focusRequester.requestFocus()
optionsViewModel.onDeleteRunSSID(ssid)
}
},
onIconClick = {
- if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus()
+ if (context.isRunningOnTv()) focusRequester.requestFocus()
optionsViewModel.onDeleteRunSSID(ssid)
},
text = ssid,
@@ -315,7 +234,7 @@ fun OptionsScreen(
Text(
stringResource(R.string.no_wifi_names_configured),
fontStyle = FontStyle.Italic,
- color = Color.Gray,
+ color = MaterialTheme.colorScheme.onSurface,
)
}
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt
index 79a6d9d..0844973 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt
@@ -3,7 +3,6 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.options
import androidx.compose.ui.util.fastFirstOrNull
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.Constants
@@ -104,7 +103,6 @@ constructor(
false -> uiState.value.tunnel
},
)
- WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
}
}
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/pinlock/PinLockScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/pinlock/PinLockScreen.kt
index 6a49483..56a9ce9 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/pinlock/PinLockScreen.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/pinlock/PinLockScreen.kt
@@ -7,10 +7,10 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavController
import com.zaneschepke.wireguardautotunnel.R
-import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.util.StringValue
+import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import xyz.teamgravity.pin_lock_compose.PinLock
@Composable
@@ -32,7 +32,7 @@ fun PinLockScreen(navController: NavController, appViewModel: AppViewModel) {
color = MaterialTheme.colorScheme.surface,
onPinCorrect = {
// pin is correct, navigate or hide pin lock
- if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
+ if (context.isRunningOnTv()) {
navController.navigate(Screen.Main.route)
} else {
val isPopped = navController.popBackStack()
@@ -52,6 +52,7 @@ fun PinLockScreen(navController: NavController, appViewModel: AppViewModel) {
appViewModel.showSnackbarMessage(
StringValue.StringResource(R.string.pin_created).asString(context),
)
+ appViewModel.onPinLockEnabled()
},
)
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt
index 8e7ac4f..77bdec4 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt
@@ -8,10 +8,10 @@ import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.provider.Settings
-import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity.RESULT_OK
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
@@ -34,8 +33,6 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.outlined.Add
-import androidx.compose.material.icons.rounded.LocationOff
-import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@@ -55,16 +52,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
-import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
@@ -81,9 +75,17 @@ import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
-import com.zaneschepke.wireguardautotunnel.util.getMessage
+import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.VpnDeniedDialog
+import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackgroundLocationDialog
+import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackgroundLocationDisclosure
+import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LocationServicesDialog
+import com.zaneschepke.wireguardautotunnel.util.extensions.getMessage
+import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
+import com.zaneschepke.wireguardautotunnel.util.extensions.launchAppSettings
+import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
import kotlinx.coroutines.launch
import timber.log.Timber
+import xyz.teamgravity.pin_lock_compose.PinManager
import java.io.File
@OptIn(
@@ -109,9 +111,11 @@ fun SettingsScreen(
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
var currentText by remember { mutableStateOf("") }
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
+ var showVpnPermissionDialog by remember { mutableStateOf(false) }
var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
var didExportFiles by remember { mutableStateOf(false) }
var showAuthPrompt by remember { mutableStateOf(false) }
+ var showLocationDialog by remember { mutableStateOf(false) }
val screenPadding = 5.dp
val fillMaxWidth = .85f
@@ -120,6 +124,13 @@ fun SettingsScreen(
viewModel.checkKernelSupport()
}
+ val notificationPermissionState =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
+ } else {
+ null
+ }
+
val startForResult =
rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
@@ -131,6 +142,19 @@ fun SettingsScreen(
viewModel.setBatteryOptimizeDisableShown()
}
+ val vpnActivityResultState =
+ rememberLauncherForActivityResult(
+ ActivityResultContracts.StartActivityForResult(),
+ onResult = {
+ val accepted = (it.resultCode == RESULT_OK)
+ if (accepted) {
+ viewModel.onToggleAutoTunnel(context)
+ } else {
+ showVpnPermissionDialog = true
+ }
+ },
+ )
+
fun exportAllConfigs() {
try {
val wgFiles =
@@ -183,13 +207,20 @@ fun SettingsScreen(
}
fun handleAutoTunnelToggle() {
- if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) {
- if (appViewModel.isRequiredPermissionGranted()) {
- viewModel.onToggleAutoTunnel(context)
- }
- } else {
- requestBatteryOptimizationsDisabled()
+ if (!uiState.isBatteryOptimizeDisableShown || !isBatteryOptimizationsDisabled()) return requestBatteryOptimizationsDisabled()
+ if (notificationPermissionState != null && !notificationPermissionState.status.isGranted) {
+ appViewModel.showSnackbarMessage(
+ context.getString(R.string.notification_permission_required),
+ )
+ return notificationPermissionState.launchPermissionRequest()
}
+ val intent = if (!uiState.settings.isKernelEnabled) {
+ com.wireguard.android.backend.GoBackend.VpnService.prepare(context)
+ } else {
+ null
+ }
+ if (intent != null) return vpnActivityResultState.launch(intent)
+ viewModel.onToggleAutoTunnel(context)
}
fun saveTrustedSSID() {
@@ -202,12 +233,6 @@ fun SettingsScreen(
}
}
- fun openSettings() {
- val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
- intentSettings.data = Uri.fromParts("package", context.packageName, null)
- context.startActivity(intentSettings)
- }
-
fun checkFineLocationGranted() {
isBackgroundLocationGranted =
if (!fineLocationState.status.isGranted) {
@@ -218,9 +243,13 @@ fun SettingsScreen(
}
}
+ fun onRootDenied() = appViewModel.showSnackbarMessage(context.getString(R.string.error_root_denied))
+
+ fun onRootAccepted() = appViewModel.showSnackbarMessage(context.getString(R.string.root_accepted))
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (
- WireGuardAutoTunnel.isRunningOnAndroidTv() &&
+ context.isRunningOnTv() &&
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q
) {
checkFineLocationGranted()
@@ -241,87 +270,30 @@ fun SettingsScreen(
checkFineLocationGranted()
}
- AnimatedVisibility(showLocationServicesAlertDialog) {
- AlertDialog(
- onDismissRequest = { showLocationServicesAlertDialog = false },
- confirmButton = {
- TextButton(
- onClick = {
- showLocationServicesAlertDialog = false
- handleAutoTunnelToggle()
- },
- ) {
- 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)) },
- )
- }
+ BackgroundLocationDisclosure(
+ !uiState.isLocationDisclosureShown,
+ onDismiss = { viewModel.setLocationDisclosureShown() },
+ onAttest = {
+ context.launchAppSettings()
+ viewModel.setLocationDisclosureShown()
+ },
+ scrollState,
+ focusRequester,
+ )
- if (!uiState.isLocationDisclosureShown) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Top,
- modifier =
- Modifier
- .fillMaxSize()
- .verticalScroll(scrollState),
- ) {
- Icon(
- Icons.Rounded.LocationOff,
- contentDescription = stringResource(id = R.string.map),
- modifier =
- Modifier
- .padding(30.dp)
- .size(128.dp),
- )
- Text(
- stringResource(R.string.prominent_background_location_title),
- textAlign = TextAlign.Center,
- modifier = Modifier.padding(30.dp),
- fontSize = 20.sp,
- )
- Text(
- stringResource(R.string.prominent_background_location_message),
- textAlign = TextAlign.Center,
- modifier = Modifier.padding(30.dp),
- fontSize = 15.sp,
- )
- Row(
- modifier =
- if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
- Modifier
- .fillMaxWidth()
- .padding(10.dp)
- } else {
- Modifier
- .fillMaxWidth()
- .padding(30.dp)
- },
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceEvenly,
- ) {
- TextButton(onClick = { viewModel.setLocationDisclosureShown() }) {
- Text(stringResource(id = R.string.no_thanks))
- }
- TextButton(
- modifier = Modifier.focusRequester(focusRequester),
- onClick = {
- openSettings()
- viewModel.setLocationDisclosureShown()
- },
- ) {
- Text(stringResource(id = R.string.turn_on))
- }
- }
- }
- }
+ BackgroundLocationDialog(
+ showLocationDialog,
+ onDismiss = { showLocationDialog = false },
+ onAttest = { showLocationDialog = false },
+ )
+
+ LocationServicesDialog(
+ showLocationServicesAlertDialog,
+ onDismiss = { showVpnPermissionDialog = false },
+ onAttest = { handleAutoTunnelToggle() },
+ )
+
+ VpnDeniedDialog(showVpnPermissionDialog, onDismiss = { showVpnPermissionDialog = false })
if (showAuthPrompt) {
AuthorizationPrompt(
@@ -344,21 +316,7 @@ fun SettingsScreen(
)
}
- if (uiState.tunnels.isEmpty() && uiState.isLocationDisclosureShown) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center,
- modifier = Modifier.fillMaxSize(),
- ) {
- Text(
- stringResource(R.string.one_tunnel_required),
- textAlign = TextAlign.Center,
- modifier = Modifier.padding(15.dp),
- fontStyle = FontStyle.Italic,
- )
- }
- }
- if (uiState.isLocationDisclosureShown && uiState.tunnels.isNotEmpty()) {
+ if (uiState.isLocationDisclosureShown) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
@@ -380,7 +338,7 @@ fun SettingsScreen(
color = MaterialTheme.colorScheme.surface,
modifier =
(
- if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
+ if (context.isRunningOnTv()) {
Modifier
.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth)
@@ -432,13 +390,13 @@ fun SettingsScreen(
uiState.settings.trustedNetworkSSIDs.forEach { ssid ->
ClickableIconButton(
onClick = {
- if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
+ if (context.isRunningOnTv()) {
focusRequester.requestFocus()
viewModel.onDeleteTrustedSSID(ssid)
}
},
onIconClick = {
- if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus()
+ if (context.isRunningOnTv()) focusRequester.requestFocus()
viewModel.onDeleteTrustedSSID(ssid)
},
text = ssid,
@@ -454,7 +412,8 @@ fun SettingsScreen(
Text(
stringResource(R.string.none),
fontStyle = FontStyle.Italic,
- color = Color.Gray,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurface,
)
}
}
@@ -560,25 +519,14 @@ fun SettingsScreen(
TextButton(
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
onClick = {
+ if (uiState.tunnels.isEmpty()) return@TextButton context.showToast(R.string.tunnel_required)
if (
uiState.settings.isTunnelOnWifiEnabled &&
!uiState.settings.isAutoTunnelEnabled
) {
when (false) {
- isBackgroundLocationGranted ->
- appViewModel.showSnackbarMessage(
- context.getString(
- R.string.background_location_required,
- ),
- )
-
- fineLocationState.status.isGranted ->
- appViewModel.showSnackbarMessage(
- context.getString(
- R.string.precise_location_required,
- ),
- )
-
+ isBackgroundLocationGranted -> showLocationDialog = true
+ fineLocationState.status.isGranted -> showLocationDialog = true
viewModel.isLocationEnabled(context) ->
showLocationServicesAlertDialog = true
@@ -648,12 +596,26 @@ fun SettingsScreen(
padding = screenPadding,
onCheckChanged = {
scope.launch {
- viewModel.onToggleKernelMode().onFailure {
- appViewModel.showSnackbarMessage(it.getMessage(context))
- }
+ viewModel.onToggleKernelMode({ onRootAccepted() }, { onRootDenied() })
}
},
)
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier =
+ Modifier
+ .fillMaxSize()
+ .padding(top = 5.dp),
+ horizontalArrangement = Arrangement.Center,
+ ) {
+ TextButton(
+ onClick = {
+ viewModel.requestRoot({ onRootAccepted() }, { onRootDenied() })
+ },
+ ) {
+ Text(stringResource(R.string.request_root))
+ }
+ }
}
}
}
@@ -677,7 +639,7 @@ fun SettingsScreen(
title = stringResource(id = R.string.other),
padding = screenPadding,
)
- if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
+ if (!context.isRunningOnTv()) {
ConfigurationToggle(
stringResource(R.string.always_on_vpn_support),
enabled = !uiState.settings.isAutoTunnelEnabled,
@@ -709,14 +671,15 @@ fun SettingsScreen(
padding = screenPadding,
onCheckChanged = {
if (uiState.isPinLockEnabled) {
- viewModel.onPinLockDisabled()
+ appViewModel.onPinLockDisabled()
} else {
- viewModel.onPinLockEnabled()
+ // TODO may want to show a dialog before proceeding in the future
+ PinManager.initialize(WireGuardAutoTunnel.instance)
navController.navigate(Screen.Lock.route)
}
},
)
- if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
+ if (!context.isRunningOnTv()) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier =
@@ -727,7 +690,10 @@ fun SettingsScreen(
) {
TextButton(
enabled = !didExportFiles,
- onClick = { showAuthPrompt = true },
+ onClick = {
+ if (uiState.tunnels.isEmpty()) return@TextButton context.showToast(R.string.tunnel_required)
+ showAuthPrompt = true
+ },
) {
Text(stringResource(R.string.export_configs))
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt
index 3b78940..1077500 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt
@@ -7,12 +7,11 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell
-import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
-import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
+import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
@@ -27,7 +26,6 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
-import xyz.teamgravity.pin_lock_compose.PinManager
import java.io.File
import javax.inject.Inject
import javax.inject.Provider
@@ -41,7 +39,7 @@ constructor(
private val rootShell: Provider,
private val fileUtils: FileUtils,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
- vpnService: VpnService,
+ tunnelService: TunnelService,
) : ViewModel() {
private val _kernelSupport = MutableStateFlow(false)
val kernelSupport = _kernelSupport.asStateFlow()
@@ -50,7 +48,7 @@ constructor(
combine(
appDataRepository.settings.getSettingsFlow(),
appDataRepository.tunnels.getTunnelConfigsFlow(),
- vpnService.vpnState,
+ tunnelService.vpnState,
appDataRepository.appState.generalStateFlow,
) { settings, tunnels, tunnelState, generalState ->
SettingsUiState(
@@ -124,7 +122,6 @@ constructor(
isAutoTunnelPaused = isAutoTunnelPaused,
),
)
- WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
}
fun onToggleAlwaysOnVPN() = viewModelScope.launch {
@@ -161,10 +158,10 @@ constructor(
)
}
- private fun saveKernelMode(on: Boolean) {
+ private fun saveKernelMode(enabled: Boolean) {
saveSettings(
uiState.value.settings.copy(
- isKernelEnabled = on,
+ isKernelEnabled = enabled,
),
)
}
@@ -192,27 +189,25 @@ constructor(
)
}
- suspend fun onToggleKernelMode(): Result {
- return withContext(ioDispatcher) {
- if (!uiState.value.settings.isKernelEnabled) {
- try {
- rootShell.get().start()
- Timber.i("Root shell accepted!")
+ fun onToggleKernelMode(onSuccess: () -> Unit, onFailure: () -> Unit) = viewModelScope.launch {
+ if (!uiState.value.settings.isKernelEnabled) {
+ requestRoot(
+ {
+ onSuccess()
saveSettings(
uiState.value.settings.copy(
isKernelEnabled = true,
isAmneziaEnabled = false,
),
)
- } catch (e: RootShell.RootShellException) {
- Timber.e(e)
- saveKernelMode(on = false)
- return@withContext Result.failure(WgTunnelExceptions.RootDenied())
- }
- } else {
- saveKernelMode(on = false)
- }
- Result.success(Unit)
+ },
+ {
+ onFailure()
+ saveKernelMode(enabled = false)
+ },
+ )
+ } else {
+ saveKernelMode(enabled = false)
}
}
@@ -234,16 +229,6 @@ constructor(
}
}
- fun onPinLockDisabled() = viewModelScope.launch {
- PinManager.clearPin()
- appDataRepository.appState.setPinLockEnabled(false)
- }
-
- fun onPinLockEnabled() = viewModelScope.launch {
- PinManager.initialize(WireGuardAutoTunnel.instance)
- appDataRepository.appState.setPinLockEnabled(true)
- }
-
fun onToggleRestartAtBoot() = viewModelScope.launch {
saveSettings(
uiState.value.settings.copy(
@@ -251,4 +236,16 @@ constructor(
),
)
}
+
+ fun requestRoot(onSuccess: () -> Unit, onFailure: () -> Unit) = viewModelScope.launch(ioDispatcher) {
+ kotlin.runCatching {
+ rootShell.get().start()
+ Timber.i("Root shell accepted!")
+ onSuccess()
+ }.onFailure {
+ onFailure()
+ }.onSuccess {
+ onSuccess()
+ }
+ }
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/BackgroundLocationDialog.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/BackgroundLocationDialog.kt
new file mode 100644
index 0000000..fdd9730
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/BackgroundLocationDialog.kt
@@ -0,0 +1,49 @@
+package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
+
+import androidx.compose.foundation.text.ClickableText
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.withStyle
+import com.zaneschepke.wireguardautotunnel.R
+import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
+import com.zaneschepke.wireguardautotunnel.util.extensions.launchAppSettings
+
+@Composable
+fun BackgroundLocationDialog(show: Boolean, onDismiss: () -> Unit, onAttest: () -> Unit) {
+ val context = LocalContext.current
+ if (show) {
+ val alwaysOnDescription = buildAnnotatedString {
+ append(stringResource(R.string.background_location_message))
+ append(" ")
+ pushStringAnnotation(tag = "appSettings", annotation = "")
+ withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) {
+ append(stringResource(id = R.string.app_settings))
+ }
+ pop()
+ append(" ")
+ append(stringResource(R.string.background_location_message2))
+ append(".")
+ }
+ InfoDialog(
+ onDismiss = { onDismiss() },
+ onAttest = { onDismiss() },
+ title = { Text(text = stringResource(R.string.vpn_denied_dialog_title)) },
+ body = {
+ ClickableText(
+ text = alwaysOnDescription,
+ style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.outline),
+ ) {
+ alwaysOnDescription.getStringAnnotations(tag = "appSettings", it, it).firstOrNull()?.let {
+ context.launchAppSettings()
+ }
+ }
+ },
+ confirmText = { Text(text = stringResource(R.string.okay)) },
+ )
+ }
+}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/BackgroundLocationDisclosure.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/BackgroundLocationDisclosure.kt
new file mode 100644
index 0000000..a111437
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/BackgroundLocationDisclosure.kt
@@ -0,0 +1,96 @@
+package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
+
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.LocationOff
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.zaneschepke.wireguardautotunnel.R
+import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
+
+@Composable
+fun BackgroundLocationDisclosure(
+ show: Boolean,
+ onDismiss: () -> Unit,
+ onAttest: () -> Unit,
+ scrollState: ScrollState,
+ focusRequester: FocusRequester,
+) {
+ val context = LocalContext.current
+ if (show) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Top,
+ modifier =
+ Modifier
+ .fillMaxSize()
+ .verticalScroll(scrollState),
+ ) {
+ Icon(
+ Icons.Rounded.LocationOff,
+ contentDescription = stringResource(id = R.string.map),
+ modifier =
+ Modifier
+ .padding(30.dp)
+ .size(128.dp),
+ )
+ Text(
+ stringResource(R.string.prominent_background_location_title),
+ textAlign = TextAlign.Center,
+ modifier = Modifier.padding(30.dp),
+ fontSize = 20.sp,
+ )
+ Text(
+ stringResource(R.string.prominent_background_location_message),
+ textAlign = TextAlign.Center,
+ modifier = Modifier.padding(30.dp),
+ fontSize = 15.sp,
+ )
+ Row(
+ modifier =
+ if (context.isRunningOnTv()) {
+ Modifier
+ .fillMaxWidth()
+ .padding(10.dp)
+ } else {
+ Modifier
+ .fillMaxWidth()
+ .padding(30.dp)
+ },
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceEvenly,
+ ) {
+ TextButton(onClick = { onDismiss() }) {
+ Text(stringResource(id = R.string.no_thanks))
+ }
+ TextButton(
+ modifier = Modifier.focusRequester(focusRequester),
+ onClick = {
+ onAttest()
+ },
+ ) {
+ Text(stringResource(id = R.string.turn_on))
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/LocationServicesDialog.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/LocationServicesDialog.kt
new file mode 100644
index 0000000..e41c930
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/LocationServicesDialog.kt
@@ -0,0 +1,34 @@
+package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
+
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import com.zaneschepke.wireguardautotunnel.R
+
+@Composable
+fun LocationServicesDialog(show: Boolean, onDismiss: () -> Unit, onAttest: () -> Unit) {
+ if (show) {
+ AlertDialog(
+ onDismissRequest = { onDismiss() },
+ confirmButton = {
+ TextButton(
+ onClick = {
+ onDismiss()
+ onAttest()
+ },
+ ) {
+ Text(text = stringResource(R.string.okay))
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = { onDismiss() }) {
+ 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)) },
+ )
+ }
+}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt
index bde52a4..be87dae 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt
@@ -48,17 +48,13 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.R
-import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
-import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Screen
+import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
+import com.zaneschepke.wireguardautotunnel.util.extensions.launchSupportEmail
+import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
@Composable
-fun SupportScreen(
- viewModel: SupportViewModel = hiltViewModel(),
- appViewModel: AppViewModel,
- navController: NavController,
- focusRequester: FocusRequester,
-) {
+fun SupportScreen(viewModel: SupportViewModel = hiltViewModel(), navController: NavController, focusRequester: FocusRequester) {
val context = LocalContext.current
val fillMaxWidth = .85f
@@ -80,7 +76,7 @@ fun SupportScreen(
color = MaterialTheme.colorScheme.surface,
modifier =
(
- if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
+ if (context.isRunningOnTv()) {
Modifier
.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth)
@@ -110,9 +106,8 @@ fun SupportScreen(
)
TextButton(
onClick = {
- appViewModel.openWebPage(
+ context.openWebUrl(
context.resources.getString(R.string.docs_url),
- context,
)
},
modifier =
@@ -153,9 +148,8 @@ fun SupportScreen(
)
TextButton(
onClick = {
- appViewModel.openWebPage(
+ context.openWebUrl(
context.resources.getString(R.string.telegram_url),
- context,
)
},
modifier = Modifier.padding(vertical = 5.dp),
@@ -173,7 +167,7 @@ fun SupportScreen(
Modifier.size(25.dp),
)
Text(
- stringResource(id = R.string.discord_description),
+ stringResource(id = R.string.chat_description),
textAlign = TextAlign.Justify,
modifier = Modifier.padding(start = 10.dp),
)
@@ -190,9 +184,8 @@ fun SupportScreen(
)
TextButton(
onClick = {
- appViewModel.openWebPage(
+ context.openWebUrl(
context.resources.getString(R.string.github_url),
- context,
)
},
modifier = Modifier.padding(vertical = 5.dp),
@@ -226,7 +219,7 @@ fun SupportScreen(
color = MaterialTheme.colorScheme.onBackground,
)
TextButton(
- onClick = { appViewModel.launchEmail(context) },
+ onClick = { context.launchSupportEmail() },
modifier = Modifier.padding(vertical = 5.dp),
) {
Row(
@@ -249,7 +242,7 @@ fun SupportScreen(
)
}
}
- if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
+ if (!context.isRunningOnTv()) {
HorizontalDivider(
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.onBackground,
@@ -288,9 +281,8 @@ fun SupportScreen(
fontSize = 16.sp,
modifier =
Modifier.clickable {
- appViewModel.openWebPage(
+ context.openWebUrl(
context.resources.getString(R.string.privacy_policy_url),
- context,
)
},
)
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsViewModel.kt
index 782970f..e619f11 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsViewModel.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsViewModel.kt
@@ -9,7 +9,7 @@ import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.MainDispatcher
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.FileUtils
-import com.zaneschepke.wireguardautotunnel.util.chunked
+import com.zaneschepke.wireguardautotunnel.util.extensions.chunked
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.launch
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Theme.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Theme.kt
index a17585f..40c6e0e 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Theme.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Theme.kt
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.theme
import android.app.Activity
import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
@@ -44,23 +45,21 @@ private val LightColorScheme =
@Composable
fun WireguardAutoTunnelTheme(
// force dark theme
- darkTheme: Boolean = true,
- // darkTheme: Boolean = isSystemInDarkTheme(),
- // Dynamic color is available on Android 12+
- // turning off dynamic color for now
- dynamicColor: Boolean = false,
+ useDarkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) {
- val colorScheme =
- when {
- dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
- val context = LocalContext.current
- if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ val context = LocalContext.current
+ val colorScheme = when {
+ (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) -> {
+ if (useDarkTheme) {
+ dynamicDarkColorScheme(context)
+ } else {
+ dynamicLightColorScheme(context)
}
-
- darkTheme -> DarkColorScheme
- else -> LightColorScheme
}
+ useDarkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
@@ -69,7 +68,7 @@ fun WireguardAutoTunnelTheme(
window.statusBarColor = Color.Transparent.toArgb()
window.navigationBarColor = Color.Transparent.toArgb()
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars =
- !darkTheme
+ !useDarkTheme
}
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt
index a81b0a9..65b9ddb 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt
@@ -7,8 +7,6 @@ object Constants {
const val MANUAL_TUNNEL_CONFIG_ID = "0"
const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1_000L // 10 minutes
const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L
- const val VPN_CONNECTED_NOTIFICATION_DELAY = 3_000L
- const val TOGGLE_TUNNEL_DELAY = 300L
const val WATCHER_COLLECTION_DELAY = 1_000L
const val CONF_FILE_EXTENSION = ".conf"
const val ZIP_FILE_EXTENSION = ".zip"
@@ -19,6 +17,7 @@ object Constants {
const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs"
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
const val ALWAYS_ON_VPN_ACTION = "android.net.VpnService"
+ const val VPN_SETTINGS_PACKAGE = "android.net.vpn.SETTINGS"
const val EMAIL_MIME_TYPE = "plain/text"
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024
@@ -31,8 +30,6 @@ object Constants {
const val PING_INTERVAL = 60_000L
const val PING_COOLDOWN = PING_INTERVAL * 60 // one hour
- const val TUNNEL_EXTRA_KEY = "tunnelId"
-
const val UNREADABLE_SSID = ""
val amneziaProperties = listOf("Jc", "Jmin", "Jmax", "S1", "S2", "H1", "H2", "H3", "H4")
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/ContextExtensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/ContextExtensions.kt
new file mode 100644
index 0000000..4c9add9
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/ContextExtensions.kt
@@ -0,0 +1,127 @@
+package com.zaneschepke.wireguardautotunnel.util.extensions
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.provider.Settings
+import android.service.quicksettings.TileService
+import android.widget.Toast
+import com.zaneschepke.wireguardautotunnel.R
+import com.zaneschepke.wireguardautotunnel.receiver.BackgroundActionReceiver
+import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
+import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
+import com.zaneschepke.wireguardautotunnel.util.Constants
+
+fun Context.openWebUrl(url: String): Result {
+ return kotlin.runCatching {
+ val webpage: Uri = Uri.parse(url)
+ val intent = Intent(Intent.ACTION_VIEW, webpage).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ startActivity(intent)
+ }.onFailure {
+ showToast(R.string.no_browser_detected)
+ }
+}
+
+fun Context.showToast(resId: Int) {
+ Toast.makeText(
+ this,
+ this.getString(resId),
+ Toast.LENGTH_LONG,
+ ).show()
+}
+
+fun Context.launchSupportEmail(): Result {
+ return runCatching {
+ val intent =
+ Intent(Intent.ACTION_SENDTO).apply {
+ type = Constants.EMAIL_MIME_TYPE
+ putExtra(Intent.EXTRA_EMAIL, arrayOf(getString(R.string.my_email)))
+ putExtra(Intent.EXTRA_SUBJECT, getString(R.string.email_subject))
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ startActivity(
+ Intent.createChooser(intent, getString(R.string.email_chooser)).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ },
+ )
+ }.onFailure {
+ showToast(R.string.no_email_detected)
+ }
+}
+
+fun Context.isRunningOnTv(): Boolean {
+ return packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
+}
+
+fun Context.launchVpnSettings(): Result {
+ return kotlin.runCatching {
+ val intent = Intent(Constants.VPN_SETTINGS_PACKAGE).apply {
+ setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ startActivity(intent)
+ }
+}
+
+fun Context.launchLocationServicesSettings(): Result {
+ return kotlin.runCatching {
+ val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS).apply {
+ setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ startActivity(intent)
+ }
+}
+
+fun Context.launchSettings(): Result {
+ return kotlin.runCatching {
+ val intent = Intent(Settings.ACTION_SETTINGS).apply {
+ setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ startActivity(intent)
+ }
+}
+
+fun Context.launchAppSettings() {
+ kotlin.runCatching {
+ val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
+ data = Uri.fromParts("package", packageName, null)
+ setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ startActivity(intent)
+ }
+}
+
+fun Context.startTunnelBackground(tunnelId: Int) {
+ sendBroadcast(
+ Intent(this, BackgroundActionReceiver::class.java).apply {
+ action = BackgroundActionReceiver.ACTION_CONNECT
+ putExtra(BackgroundActionReceiver.TUNNEL_ID_EXTRA_KEY, tunnelId)
+ },
+ )
+}
+
+fun Context.stopTunnelBackground(tunnelId: Int) {
+ sendBroadcast(
+ Intent(this, BackgroundActionReceiver::class.java).apply {
+ action = BackgroundActionReceiver.ACTION_DISCONNECT
+ putExtra(BackgroundActionReceiver.TUNNEL_ID_EXTRA_KEY, tunnelId)
+ },
+ )
+}
+
+fun Context.requestTunnelTileServiceStateUpdate() {
+ TileService.requestListeningState(
+ this,
+ ComponentName(this, TunnelControlTile::class.java),
+ )
+}
+
+fun Context.requestAutoTunnelTileServiceUpdate() {
+ TileService.requestListeningState(
+ this,
+ ComponentName(this, AutoTunnelControlTile::class.java),
+ )
+}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/CoroutineExtensions.kt
similarity index 52%
rename from app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt
rename to app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/CoroutineExtensions.kt
index cc4a299..d56586d 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/CoroutineExtensions.kt
@@ -1,11 +1,5 @@
-package com.zaneschepke.wireguardautotunnel.util
+package com.zaneschepke.wireguardautotunnel.util.extensions
-import android.content.Context
-import android.content.pm.PackageInfo
-import com.zaneschepke.wireguardautotunnel.R
-import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
-import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
-import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -18,71 +12,11 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.selects.whileSelect
-import org.amnezia.awg.config.Config
import timber.log.Timber
-import java.math.BigDecimal
-import java.text.DecimalFormat
import java.time.Duration
import java.util.concurrent.ConcurrentLinkedQueue
import kotlin.coroutines.cancellation.CancellationException
-fun BigDecimal.toThreeDecimalPlaceString(): String {
- val df = DecimalFormat("#.###")
- return df.format(this)
-}
-
-fun List.update(index: Int, item: T): List = toMutableList().apply { this[index] = item }
-
-fun List.removeAt(index: Int): List = toMutableList().apply { this.removeAt(index) }
-
-typealias TunnelConfigs = List
-
-typealias Packages = List
-
-fun TunnelStatistics.mapPeerStats(): Map {
- return this.getPeers().associateWith { key -> (this.peerStats(key)) }
-}
-
-fun TunnelStatistics.PeerStats.latestHandshakeSeconds(): Long? {
- return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis)
-}
-
-fun TunnelStatistics.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
- }
- }
- }
-}
-
-fun Config.toWgQuickString(): String {
- val amQuick = toAwgQuickString()
- val lines = amQuick.lines().toMutableList()
- val linesIterator = lines.iterator()
- while (linesIterator.hasNext()) {
- val next = linesIterator.next()
- Constants.amneziaProperties.forEach {
- if (next.startsWith(it, ignoreCase = true)) {
- linesIterator.remove()
- }
- }
- }
- return lines.joinToString(System.lineSeparator())
-}
-
-fun Throwable.getMessage(context: Context): String {
- return when (this) {
- is WgTunnelExceptions -> this.getMessage(context)
- else -> this.message ?: StringValue.StringResource(R.string.unknown_error).asString(context)
- }
-}
-
/**
* Chunks based on a time or size threshold.
*
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/Extensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/Extensions.kt
new file mode 100644
index 0000000..bde4156
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/Extensions.kt
@@ -0,0 +1,30 @@
+package com.zaneschepke.wireguardautotunnel.util.extensions
+
+import android.content.Context
+import android.content.pm.PackageInfo
+import com.zaneschepke.wireguardautotunnel.R
+import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
+import com.zaneschepke.wireguardautotunnel.util.StringValue
+import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
+import java.math.BigDecimal
+import java.text.DecimalFormat
+
+fun BigDecimal.toThreeDecimalPlaceString(): String {
+ val df = DecimalFormat("#.###")
+ return df.format(this)
+}
+
+fun List.update(index: Int, item: T): List = toMutableList().apply { this[index] = item }
+
+fun List.removeAt(index: Int): List = toMutableList().apply { this.removeAt(index) }
+
+typealias TunnelConfigs = List
+
+typealias Packages = List
+
+fun Throwable.getMessage(context: Context): String {
+ return when (this) {
+ is WgTunnelExceptions -> this.getMessage(context)
+ else -> this.message ?: StringValue.StringResource(R.string.unknown_error).asString(context)
+ }
+}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt
new file mode 100644
index 0000000..19a0006
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt
@@ -0,0 +1,44 @@
+package com.zaneschepke.wireguardautotunnel.util.extensions
+
+import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
+import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
+import com.zaneschepke.wireguardautotunnel.util.Constants
+import com.zaneschepke.wireguardautotunnel.util.NumberUtils
+import org.amnezia.awg.config.Config
+
+fun TunnelStatistics.mapPeerStats(): Map {
+ return this.getPeers().associateWith { key -> (this.peerStats(key)) }
+}
+
+fun TunnelStatistics.PeerStats.latestHandshakeSeconds(): Long? {
+ return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis)
+}
+
+fun TunnelStatistics.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
+ }
+ }
+ }
+}
+
+fun Config.toWgQuickString(): String {
+ val amQuick = toAwgQuickString()
+ val lines = amQuick.lines().toMutableList()
+ val linesIterator = lines.iterator()
+ while (linesIterator.hasNext()) {
+ val next = linesIterator.next()
+ Constants.amneziaProperties.forEach {
+ if (next.startsWith(it, ignoreCase = true)) {
+ linesIterator.remove()
+ }
+ }
+ }
+ return lines.joinToString(System.lineSeparator())
+}
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index 1a51b16..ff3fdc0 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -6,8 +6,7 @@
Přidat název důvěryhodné Wi-Fi
Spustit automatické tunelování
Tunelování na mobilních datech
- Alespoň jeden tunel je vyžadován pro použití této funkce
- Otevřít zásady soukromí
+ Otevřít zásady soukromí
OK
Děkujeme za používání WG Tunnel!
Přidat ze souboru nebo zipu
@@ -116,7 +115,6 @@
Kopírovat veřejný klíč
base64 klíč
Přečíst si dokumentaci
- Přidat se ke komunitě
Poslat mi email
Pokud máte potíže, nápady pro zlepšení, nebo se chcete jen zapojit, následující prostředky jsou k dispozici:
Aplikace nenašla žádné služby polohy zapnuté na Vašem zařízení. Dle Vašeho zařízení, tohle může způsobit, že funkce nedůvěryhodné Wi-Fi nedokáže přečíst jméno připojené Wi-Fi. Chcete i přesto pokračovat?
@@ -151,4 +149,4 @@
(automaticky)
Kernel
Backend
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 700feae..2c9ec7a 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -7,8 +7,7 @@
Tunnel
Auto-Tunneln starten
Tunnel für mobile Daten
- Mindestens ein Tunnel wird für diese Funktion benötigt
- Datenschutzbestimmungen anzeigen
+ Datenschutzbestimmungen anzeigen
Auto-Tunneln stoppen
Ok
Tunnel für Ethernet
@@ -75,7 +74,6 @@
Dauerhaftes Keepalive
Hintergrund Standortdienste erforderlich
App-Sperre aktivieren
- Tritt der Community bei
Schnittstelle
Eingehender Port
(zufällig)
@@ -168,4 +166,4 @@
Konfigurationsexport fehlgeschlagen
Ungültige Konfiguration Tunnel-Format
Antwortpaket magic header
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index eea7141..6eb6879 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -4,7 +4,6 @@
Allowed IPs
Enviar un email…
ir
- Únete a la comunidad
Kernel
Reiniciar túnel
Error de conexión
@@ -60,8 +59,7 @@
Iniciar túnel-automático
Parar túnel-automático
Activar túnel en datos móviles
- Esta característica necesita ser usada en almenos por un túnel
- Ver Política de Privacidad
+ Ver Política de Privacidad
OK
Túnel en ethernet
Divulgación de la ubicación en segundo plano
@@ -157,4 +155,4 @@
Recuento de paquetes basura
Backend
Tamaño mínimo del paquete basura
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index e09b555..33e1f25 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -56,8 +56,7 @@
Ver a Política de Privacidade
OK
Túnel em dados móveis
- Pelo menos um túnel é necessário para usar este recurso
- Revelar a localização em segundo plano
+ Revelar a localização em segundo plano
Obrigado por usar o WG Tunnel!
Envie o SSID
Abrir Arquivo
@@ -119,7 +118,6 @@
Apagar túnel
Enviar um email…
Usar o módulo do kernel
- Juntar-se à comunidade
Ler a documentação
Se você enfrentar problemas, tiver ideias para melhorias ou apenas quiser participar, os seguintes recursos estão disponíveis:
Shell Root negado
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index bb4ac2d..8d697e3 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -58,7 +58,6 @@
Туннелировать через Ethernet
Отмена
Посмотреть документацию
- Присоединиться к сообществу
Отправить письмо…
Добавить доверенное имя сети Wi-Fi
включено
@@ -88,8 +87,7 @@
Туннели
Запустить авто-туннель
Остановить авто-туннель
- Для использования этой функции нужно настроить хотя бы один туннель
- Хорошо
+ Хорошо
Фоновая передача местоположения
Благодарим Вас за использование WG Tunnel!
Отправить SSID
@@ -168,4 +166,4 @@
руководство по началу работы
, если не уверены, что делать дальше
Заголовок пакета под нагрузкой
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 302de02..8997998 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -21,8 +21,7 @@
Otomatik tünellemeyi başlat
Otomatik tünellemeyi durdur
Mobil veride tünel
- Bu özelliği kullanmak için en az bir tünel gerekli
- Gizlilik Politikasını Görüntüle
+ Gizlilik Politikasını Görüntüle
Tamam
Ethernet\'te tünel
Bu özellik, uygulama kapalıyken bile Wi-Fi SSID izlemesini etkinleştirmek için arka plan konum iznine ihtiyaç duyar. Daha fazla ayrıntı için lütfen Destek ekranında bağlantısı verilen Gizlilik Politikasına bakın.
@@ -104,7 +103,6 @@
E-posta gönder…
git
Belgeleri oku
- Topluluğa katıl
Bana e-posta gönder
Sorun yaşıyorsanız, iyileştirme fikirleriniz varsa veya sadece iletişime geçmek istiyorsanız, aşağıdaki kaynaklar mevcuttur:
Kernel modülünü kullan
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index 46d0933..96effb2 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -56,8 +56,7 @@
VPN підключено
Потрібен дозвіл на відображення сповіщень.
Тунелювати мобільні дані
- Для використання даної функції потрібно налаштувати мінімум один тунель
- Переглянути політику конфіденційності
+ Переглянути політику конфіденційності
Спасибі за використання WG Tunnel!
Дана функція потребує фоновий доступ до служби місцезнаходження для моніторингу назви мереж Wi-Fi навіть коли додаток закрито. Для отримання додаткової інформації прочитайте політику приватності на екрані Підтримки.
Введіть SSID
@@ -112,7 +111,6 @@
вперед
Переглянути документацію
Відправити email автору
- Приєднатися до спільноти
Якщо у вас виникли проблеми, є ідеї щодо покращення, чи бажання долучитися, скористайтесь наступними ресурсами:
Використовувати модуль режиму ядра
SSID вже існує
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ccb18e3..e0b2dfd 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -21,8 +21,7 @@
Start auto-tunneling
Stop auto-tunneling
Tunnel on mobile data
- At least one tunnel required to use this feature
- View Privacy Policy
+ View Privacy Policy
Okay
Tunnel on ethernet
This feature requires background location permission to enable Wi-Fi SSID monitoring even while the application is closed. For more details, please see the Privacy Policy linked on the Support screen.
@@ -104,8 +103,7 @@
Send an email…
go
Read the docs
- Join the community
- Send me an email
+ Send me an email
If you are experiencing issues, have improvement ideas, or just want to engage, the following resources are available:
Use kernel module
SSID already exists
@@ -178,6 +176,14 @@
Invalid tunnel config format
Restart on boot
Permission Denied
- Permission to start the VPN has either been explicitly denied or is being blocked by the system.
- If VPN permission is being blocked by the system, please confirm no other app is using the Always-on VPN feature and try again.
+ VPN system settings
+ VPN connection permission has been denied. Please check the
+ to make sure Always-on VPN is turned off for all other apps and try again
+ Join the community
+ Feature requires at least one tunnel
+ Request root
+ Allow all the time location permission and/or precise location is required for this feature. Please see
+ app settings
+ to make sure these permissions are enabled.
+ Root shell accepted
diff --git a/app/src/main/res/xml/shortcuts.xml b/app/src/main/res/xml/shortcuts.xml
index f6f7d72..c4f61c1 100644
--- a/app/src/main/res/xml/shortcuts.xml
+++ b/app/src/main/res/xml/shortcuts.xml
@@ -4,7 +4,7 @@
android:icon="@drawable/vpn_on"
android:shortcutDisabledMessage="@string/vpn_on"
android:shortcutId="defaultOn1"
- android:shortcutLongLabel="@string/default_vpn_on"
+ android:shortcutLongLabel="@string/vpn_on"
android:shortcutShortLabel="@string/vpn_on">