From bfb8d5982759b110b0dab742a2cabd279b49a4e2 Mon Sep 17 00:00:00 2001 From: Zane Schepke Date: Sat, 10 Aug 2024 23:59:05 -0400 Subject: [PATCH] fix: improve tunnel reliability (#298) - Attempts to fix tunnel and auto-tunnel reliability by removing the tunnel foreground service and circumventing the limitation of starting the vpn service from by background by using a broadcast receiver. - Removes tunnel foreground notification. - Improves the reliability auto-tunnel start on reboot by adding an additional notification launch calls. - Fixes bug where pin feature could be turned on without the pin being set. - Improves quick tile reliability and sync. - Improves reliability of app shortcuts. - Improves kernel mode - Improves permissions flow - Adds support for dynamic app colors Android 12+ - Add support for light/dark system modes --- .gitignore | 1 + app/build.gradle.kts | 1 + .../9.json | 197 +++++++++ app/src/main/AndroidManifest.xml | 53 ++- .../WireGuardAutoTunnel.kt | 31 +- .../wireguardautotunnel/data/AppDatabase.kt | 3 +- .../data/TunnelConfigDao.kt | 5 +- .../data/datastore/DataStoreManager.kt | 4 +- .../data/domain/GeneralState.kt | 4 +- .../data/domain/TunnelConfig.kt | 9 + .../data/repository/AppDataRepository.kt | 2 - .../data/repository/AppDataRoomRepository.kt | 22 +- .../data/repository/AppStateRepository.kt | 8 +- .../repository/DataStoreAppStateRepository.kt | 68 ++- .../data/repository/RoomSettingsRepository.kt | 15 +- .../repository/RoomTunnelConfigRepository.kt | 76 ++-- .../data/repository/TunnelConfigRepository.kt | 4 +- .../module/RepositoryModule.kt | 12 +- .../module/TunnelModule.kt | 25 +- .../receiver/BackgroundActionReceiver.kt | 56 +++ .../receiver/BootReceiver.kt | 40 +- .../receiver/KernelReceiver.kt | 48 +++ .../receiver/NotificationActionReceiver.kt | 44 -- ...WatcherService.kt => AutoTunnelService.kt} | 185 ++++----- .../{WatcherState.kt => AutoTunnelState.kt} | 2 +- .../service/foreground/ServiceManager.kt | 72 +--- .../foreground/WireGuardTunnelService.kt | 198 --------- .../service/shortcut/ShortcutsActivity.kt | 46 +-- .../service/tile/AutoTunnelControlTile.kt | 137 +++--- .../service/tile/TunnelControlTile.kt | 131 +++--- .../service/tunnel/AlwaysOnVpnService.kt | 46 +++ .../service/tunnel/TunnelService.kt | 17 + .../service/tunnel/VpnService.kt | 15 - .../service/tunnel/WireGuardTunnel.kt | 185 ++++----- .../wireguardautotunnel/ui/AppUiState.kt | 1 - .../wireguardautotunnel/ui/AppViewModel.kt | 100 +---- .../wireguardautotunnel/ui/MainActivity.kt | 88 ---- .../wireguardautotunnel/ui/SplashActivity.kt | 33 +- .../ui/common/RowListItem.kt | 2 +- .../ui/common/functions/Functions.kt | 53 +++ .../ui/common/navigation/BottomNavBar.kt | 15 +- .../ui/common/prompt/CustomSnackbar.kt | 10 +- .../ui/screens/config/ConfigScreen.kt | 21 +- .../ui/screens/config/ConfigUiState.kt | 2 +- .../ui/screens/config/ConfigViewModel.kt | 7 +- .../ui/screens/main/MainScreen.kt | 390 ++++-------------- .../ui/screens/main/MainUiState.kt | 2 +- .../ui/screens/main/MainViewModel.kt | 34 +- .../main/components/GettingStartedLabel.kt | 71 ++++ .../main/components/ScrollDismissMultiFab.kt | 108 +++++ .../main/components/TunnelImportSheet.kt | 104 +++++ .../main/components/VpnDeniedDialog.kt | 49 +++ .../ui/screens/options/OptionsScreen.kt | 109 +---- .../ui/screens/options/OptionsViewModel.kt | 2 - .../ui/screens/pinlock/PinLockScreen.kt | 5 +- .../ui/screens/settings/SettingsScreen.kt | 250 +++++------ .../ui/screens/settings/SettingsViewModel.kt | 63 ++- .../components/BackgroundLocationDialog.kt | 49 +++ .../BackgroundLocationDisclosure.kt | 96 +++++ .../components/LocationServicesDialog.kt | 34 ++ .../ui/screens/support/SupportScreen.kt | 32 +- .../ui/screens/support/logs/LogsViewModel.kt | 2 +- .../wireguardautotunnel/ui/theme/Theme.kt | 27 +- .../wireguardautotunnel/util/Constants.kt | 5 +- .../util/extensions/ContextExtensions.kt | 127 ++++++ .../CoroutineExtensions.kt} | 68 +-- .../util/extensions/Extensions.kt | 30 ++ .../util/extensions/TunnelExtensions.kt | 44 ++ app/src/main/res/values-cs/strings.xml | 6 +- app/src/main/res/values-de/strings.xml | 6 +- app/src/main/res/values-es/strings.xml | 6 +- app/src/main/res/values-pt/strings.xml | 4 +- app/src/main/res/values-ru/strings.xml | 6 +- app/src/main/res/values-tr/strings.xml | 4 +- app/src/main/res/values-uk/strings.xml | 4 +- app/src/main/res/values/strings.xml | 18 +- app/src/main/res/xml/shortcuts.xml | 4 +- buildSrc/src/main/kotlin/Constants.kt | 6 +- buildSrc/src/main/kotlin/Extensions.kt | 1 - gradle/libs.versions.toml | 14 +- 80 files changed, 2066 insertions(+), 1808 deletions(-) create mode 100644 app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/9.json create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BackgroundActionReceiver.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/KernelReceiver.kt delete mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt rename app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/{WireGuardConnectivityWatcherService.kt => AutoTunnelService.kt} (75%) rename app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/{WatcherState.kt => AutoTunnelState.kt} (98%) delete mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/AlwaysOnVpnService.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/TunnelService.kt delete mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnService.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/functions/Functions.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/GettingStartedLabel.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/ScrollDismissMultiFab.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelImportSheet.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/VpnDeniedDialog.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/BackgroundLocationDialog.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/BackgroundLocationDisclosure.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/LocationServicesDialog.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/ContextExtensions.kt rename app/src/main/java/com/zaneschepke/wireguardautotunnel/util/{Extensions.kt => extensions/CoroutineExtensions.kt} (52%) create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/Extensions.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt 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">