diff --git a/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/11.json b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/11.json new file mode 100644 index 0000000..a477649 --- /dev/null +++ b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/11.json @@ -0,0 +1,232 @@ +{ + "formatVersion": 1, + "database": { + "version": 11, + "identityHash": "4c9418386f72dfac5d28ab96c1e5ea0b", + "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_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_wifi_by_shell_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": "isPingEnabled", + "columnName": "is_ping_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isAmneziaEnabled", + "columnName": "is_amnezia_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isWildcardsEnabled", + "columnName": "is_wildcards_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isWifiNameByShellEnabled", + "columnName": "is_wifi_by_shell_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, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wgQuick", + "columnName": "wg_quick", + "affinity": "TEXT", + "notNull": true + }, + { + "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" + }, + { + "fieldPath": "isPingEnabled", + "columnName": "is_ping_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "pingInterval", + "columnName": "ping_interval", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "null" + }, + { + "fieldPath": "pingCooldown", + "columnName": "ping_cooldown", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "null" + }, + { + "fieldPath": "pingIp", + "columnName": "ping_ip", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "null" + } + ], + "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, '4c9418386f72dfac5d28ab96c1e5ea0b')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 180c821..7c81484 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -186,10 +186,6 @@ - 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 cc06085..052a5d7 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 = 10, + version = 11, autoMigrations = [ AutoMigration(from = 1, to = 2), @@ -36,6 +36,11 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig AutoMigration(7, 8), AutoMigration(8, 9), AutoMigration(9, 10), + AutoMigration( + from = 10, + to = 11, + spec = RemoveTunnelPauseMigration::class, + ), ], exportSchema = true, ) @@ -55,3 +60,9 @@ abstract class AppDatabase : RoomDatabase() { columnName = "is_battery_saver_enabled", ) class RemoveLegacySettingColumnsMigration : AutoMigrationSpec + +@DeleteColumn( + tableName = "Settings", + columnName = "is_auto_tunnel_paused", +) +class RemoveTunnelPauseMigration : AutoMigrationSpec 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 54c80c3..5ed880d 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 @@ -26,7 +26,6 @@ class DataStoreManager( val currentSSID = stringPreferencesKey("CURRENT_SSID") val pinLockEnabled = booleanPreferencesKey("PIN_LOCK_ENABLED") val tunnelStatsExpanded = booleanPreferencesKey("TUNNEL_STATS_EXPANDED") - val wildcardsEnabled = booleanPreferencesKey("WILDCARDS_ENABLED") val theme = stringPreferencesKey("THEME") } 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 db7be88..59e4b15 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 @@ -7,14 +7,12 @@ data class GeneralState( val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT, val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT, val isTunnelStatsExpanded: Boolean = IS_TUNNEL_STATS_EXPANDED, - val isWildcardsEnabled: Boolean = IS_WILDCARDS_ENABLED, - val theme: Theme = Theme.AUTOMATIC + val theme: Theme = Theme.AUTOMATIC, ) { companion object { const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false const val PIN_LOCK_ENABLED_DEFAULT = false const val IS_TUNNEL_STATS_EXPANDED = false - const val IS_WILDCARDS_ENABLED = false } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/Settings.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/Settings.kt index bec24e9..aceda14 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/Settings.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/Settings.kt @@ -40,11 +40,6 @@ data class Settings( defaultValue = "false", ) val isMultiTunnelEnabled: Boolean = false, - @ColumnInfo( - name = "is_auto_tunnel_paused", - defaultValue = "false", - ) - val isAutoTunnelPaused: Boolean = false, @ColumnInfo( name = "is_ping_enabled", defaultValue = "false", @@ -55,4 +50,14 @@ data class Settings( defaultValue = "false", ) val isAmneziaEnabled: Boolean = false, + @ColumnInfo( + name = "is_wildcards_enabled", + defaultValue = "false", + ) + val isWildcardsEnabled: Boolean = false, + @ColumnInfo( + name = "is_wifi_by_shell_enabled", + defaultValue = "false", + ) + val isWifiNameByShellEnabled: Boolean = false, ) 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 142465c..7e5f007 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 @@ -13,10 +13,6 @@ interface AppStateRepository { suspend fun setPinLockEnabled(enabled: Boolean) - suspend fun isWildcardsEnabled(): Boolean - - suspend fun setWildcardsEnabled(enabled: Boolean) - suspend fun isBatteryOptimizationDisableShown(): Boolean suspend fun setBatteryOptimizationDisableShown(shown: Boolean) @@ -31,7 +27,7 @@ interface AppStateRepository { suspend fun setTheme(theme: Theme) - suspend fun getTheme() : Theme + suspend fun getTheme(): Theme val generalStateFlow: Flow } 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 58260a6..39dcfc8 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 @@ -29,14 +29,6 @@ class DataStoreAppStateRepository( dataStoreManager.saveToDataStore(DataStoreManager.pinLockEnabled, enabled) } - override suspend fun isWildcardsEnabled(): Boolean { - return dataStoreManager.getFromStore(DataStoreManager.wildcardsEnabled) ?: GeneralState.IS_WILDCARDS_ENABLED - } - - override suspend fun setWildcardsEnabled(enabled: Boolean) { - dataStoreManager.saveToDataStore(DataStoreManager.wildcardsEnabled, enabled) - } - override suspend fun isBatteryOptimizationDisableShown(): Boolean { return dataStoreManager.getFromStore(DataStoreManager.batteryDisableShown) ?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT @@ -71,7 +63,7 @@ class DataStoreAppStateRepository( return dataStoreManager.getFromStore(DataStoreManager.theme)?.let { try { Theme.valueOf(it) - } catch (_ : IllegalArgumentException) { + } catch (_: IllegalArgumentException) { Theme.AUTOMATIC } } ?: Theme.AUTOMATIC @@ -92,8 +84,7 @@ class DataStoreAppStateRepository( pref[DataStoreManager.pinLockEnabled] ?: GeneralState.PIN_LOCK_ENABLED_DEFAULT, isTunnelStatsExpanded = pref[DataStoreManager.tunnelStatsExpanded] ?: GeneralState.IS_TUNNEL_STATS_EXPANDED, - isWildcardsEnabled = pref[DataStoreManager.wildcardsEnabled] ?: GeneralState.IS_WILDCARDS_ENABLED, - theme = getTheme() + theme = getTheme(), ) } catch (e: IllegalArgumentException) { Timber.e(e) 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 bf63974..eeae7d5 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,11 +1,9 @@ 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.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 @@ -26,8 +24,6 @@ class RoomTunnelConfigRepository( override suspend fun save(tunnelConfig: TunnelConfig) { withContext(ioDispatcher) { tunnelConfigDao.save(tunnelConfig) - }.also { - WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate() } } @@ -60,8 +56,6 @@ class RoomTunnelConfigRepository( override suspend fun delete(tunnelConfig: TunnelConfig) { withContext(ioDispatcher) { tunnelConfigDao.delete(tunnelConfig) - }.also { - WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate() } } 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 b14c118..e8bf218 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt @@ -9,6 +9,7 @@ import com.wireguard.android.util.RootShell import com.wireguard.android.util.ToolsInstaller import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository +import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel import dagger.Module @@ -65,6 +66,7 @@ class TunnelModule { tunnelConfigRepository: TunnelConfigRepository, @ApplicationScope applicationScope: CoroutineScope, @IoDispatcher ioDispatcher: CoroutineDispatcher, + serviceManager: ServiceManager, ): TunnelService { return WireGuardTunnel( amneziaBackend, @@ -73,6 +75,13 @@ class TunnelModule { appDataRepository, applicationScope, ioDispatcher, + serviceManager, ) } + + @Singleton + @Provides + fun provideServiceManager(@ApplicationContext context: Context): ServiceManager { + return ServiceManager(context) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/AppUpdateReceiver.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/AppUpdateReceiver.kt index b60e9a4..14f68ed 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/AppUpdateReceiver.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/AppUpdateReceiver.kt @@ -7,12 +7,12 @@ 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 com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground 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 AppUpdateReceiver : BroadcastReceiver() { @@ -25,7 +25,10 @@ class AppUpdateReceiver : BroadcastReceiver() { lateinit var appDataRepository: AppDataRepository @Inject - lateinit var tunnelService: TunnelService + lateinit var tunnelService: Provider + + @Inject + lateinit var serviceManager: ServiceManager override fun onReceive(context: Context, intent: Intent) { if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return @@ -33,11 +36,11 @@ class AppUpdateReceiver : BroadcastReceiver() { val settings = appDataRepository.settings.getSettings() if (settings.isAutoTunnelEnabled) { Timber.i("Restarting services after upgrade") - ServiceManager.startWatcherServiceForeground(context) + serviceManager.startAutoTunnel(true) } - if (!settings.isAutoTunnelEnabled || settings.isAutoTunnelPaused) { + if (!settings.isAutoTunnelEnabled) { val tunnels = appDataRepository.tunnels.getAll().filter { it.isActive } - if (tunnels.isNotEmpty()) context.startTunnelBackground(tunnels.first().id) + if (tunnels.isNotEmpty()) tunnelService.get().startTunnel(tunnels.first(), true) } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BackgroundActionReceiver.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BackgroundActionReceiver.kt deleted file mode 100644 index aea9c42..0000000 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BackgroundActionReceiver.kt +++ /dev/null @@ -1,61 +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.TunnelConfigRepository -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 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 -> { - Timber.d("Connect actions") - applicationScope.launch { - val tunnel = tunnelConfigRepository.getById(id) - tunnel?.let { - ServiceManager.startTunnelBackgroundService(context) - tunnelService.get().startTunnel(it) - } - } - } - ACTION_DISCONNECT -> { - applicationScope.launch { - val tunnel = tunnelConfigRepository.getById(id) - tunnel?.let { - ServiceManager.stopTunnelBackgroundService(context) - 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 eb03f3e..f5b37e0 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt @@ -8,7 +8,6 @@ import com.zaneschepke.wireguardautotunnel.module.ApplicationScope import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState -import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -28,6 +27,9 @@ class BootReceiver : BroadcastReceiver() { @ApplicationScope lateinit var applicationScope: CoroutineScope + @Inject + lateinit var serviceManager: ServiceManager + override fun onReceive(context: Context, intent: Intent) { if (Intent.ACTION_BOOT_COMPLETED != intent.action) return applicationScope.launch { @@ -37,11 +39,11 @@ class BootReceiver : BroadcastReceiver() { val tunState = tunnelService.get().vpnState.value.status if (activeTunnels.isNotEmpty() && tunState != TunnelState.UP) { Timber.i("Starting previously active tunnel") - context.startTunnelBackground(activeTunnels.first().id) + tunnelService.get().startTunnel(activeTunnels.first(), true) } if (isAutoTunnelEnabled) { Timber.i("Starting watcher service from boot") - ServiceManager.startWatcherServiceForeground(context) + serviceManager.startAutoTunnel(true) } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/AutoTunnelService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/AutoTunnelService.kt index cfbc88d..133382b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/AutoTunnelService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/AutoTunnelService.kt @@ -26,10 +26,11 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName -import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList +import com.zaneschepke.wireguardautotunnel.util.extensions.isDown import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable import com.zaneschepke.wireguardautotunnel.util.extensions.onNotRunning import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -74,6 +75,9 @@ class AutoTunnelService : LifecycleService() { @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher + @Inject + lateinit var serviceManager: ServiceManager + @Inject @MainImmediateDispatcher lateinit var mainImmediateDispatcher: CoroutineDispatcher @@ -88,14 +92,11 @@ class AutoTunnelService : LifecycleService() { private var pingJob: Job? = null private var networkEventJob: Job? = null - @get:Synchronized @set:Synchronized - private var running: Boolean = false - override fun onCreate() { super.onCreate() lifecycleScope.launch(mainImmediateDispatcher) { kotlin.runCatching { - launchNotification() + launchWatcherNotification() }.onFailure { Timber.e(it) } @@ -110,32 +111,14 @@ class AutoTunnelService : LifecycleService() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Timber.d("onStartCommand executed with startId: $startId") - if (intent != null) { - val action = intent.action - when (action) { - Action.START.name, - Action.START_FOREGROUND.name, - -> startService() - Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService() - } - } + serviceManager.autoTunnelService.complete(this) return super.onStartCommand(intent, flags, startId) } - private suspend fun launchNotification() { - if (appDataRepository.settings.getSettings().isAutoTunnelPaused) { - launchWatcherPausedNotification() - } else { - launchWatcherNotification() - } - } - - private fun startService() { - if (running) return - running = true + fun start() { kotlin.runCatching { lifecycleScope.launch(mainImmediateDispatcher) { - launchNotification() + launchWatcherNotification() initWakeLock() } startSettingsJob() @@ -145,7 +128,7 @@ class AutoTunnelService : LifecycleService() { } } - private fun stopService() { + fun stop() { wakeLock?.let { if (it.isHeld) { it.release() @@ -157,6 +140,7 @@ class AutoTunnelService : LifecycleService() { override fun onDestroy() { cancelAndResetNetworkJobs() cancelAndResetPingJob() + serviceManager.autoTunnelService = CompletableDeferred() super.onDestroy() } @@ -176,10 +160,6 @@ class AutoTunnelService : LifecycleService() { ) } - private fun launchWatcherPausedNotification() { - launchWatcherNotification(getString(R.string.watcher_notification_text_paused)) - } - private fun initWakeLock() { wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).run { @@ -265,8 +245,7 @@ class AutoTunnelService : LifecycleService() { runCatching { do { val vpnState = tunnelService.get().vpnState.value - val settings = appDataRepository.settings.getSettings() - if (vpnState.status == TunnelState.UP && !settings.isAutoTunnelPaused) { + if (vpnState.status == TunnelState.UP) { if (vpnState.tunnelConfig != null) { val config = TunnelConfig.configFromWgQuick(vpnState.tunnelConfig.wgQuick) val results = if (vpnState.tunnelConfig.pingIp != null) { @@ -296,17 +275,6 @@ class AutoTunnelService : LifecycleService() { } } - private fun onAutoTunnelPause(paused: Boolean) { - if (autoTunnelStateFlow.value.settings.isAutoTunnelPaused - != paused - ) { - when (paused) { - true -> launchWatcherPausedNotification() - false -> launchWatcherNotification() - } - } - } - private suspend fun watchForSettingsChanges() { Timber.i("Starting settings watcher") withContext(ioDispatcher) { @@ -321,7 +289,7 @@ class AutoTunnelService : LifecycleService() { tunnels = tunnels, ) }.collect { - onAutoTunnelPause(it.settings.isAutoTunnelPaused) + Timber.d("got new settings: ${it.settings}") manageJobsBySettings(it.settings) autoTunnelStateFlow.emit(it) } @@ -331,7 +299,12 @@ class AutoTunnelService : LifecycleService() { private suspend fun watchForVpnStateChanges() { Timber.i("Starting vpn state watcher") withContext(ioDispatcher) { - tunnelService.get().vpnState.collect { state -> + tunnelService.get().vpnState.distinctUntilChanged { old, new -> + old.tunnelConfig == new.tunnelConfig && old.status == new.status + }.collect { state -> + autoTunnelStateFlow.update { + it.copy(vpnState = state) + } state.tunnelConfig?.let { val settings = appDataRepository.settings.getSettings() if (it.isPingEnabled && !settings.isPingEnabled) { @@ -475,9 +448,8 @@ class AutoTunnelService : LifecycleService() { private suspend fun getWifiSSID(networkCapabilities: NetworkCapabilities): String? { return withContext(ioDispatcher) { - try { - rootShell.get().getCurrentWifiName() - } catch (_: Exception) { + with(autoTunnelStateFlow.value.settings) { + if (isWifiNameByShellEnabled) return@withContext rootShell.get().getCurrentWifiName() wifiService.getNetworkName(networkCapabilities) } } @@ -487,100 +459,95 @@ class AutoTunnelService : LifecycleService() { return appDataRepository.tunnels.findByMobileDataTunnel().firstOrNull() } - private fun isTunnelDown(): Boolean { - return tunnelService.get().vpnState.value.status == TunnelState.DOWN - } - private suspend fun handleNetworkEventChanges() { withContext(ioDispatcher) { Timber.i("Starting network event watcher") autoTunnelStateFlow.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 activeTunnel = tunnelService.get().vpnState.value.tunnelConfig - val defaultTunnel = appDataRepository.getPrimaryOrFirstTunnel() - when { - watcherState.isEthernetConditionMet() -> { - Timber.i("$autoTunnel - tunnel on on ethernet condition met") - if (isTunnelDown()) { - defaultTunnel?.let { + Timber.d("New watcher state!") + // delay for rapid network state changes and then collect latest + delay(Constants.WATCHER_COLLECTION_DELAY) + val activeTunnel = watcherState.vpnState.tunnelConfig + val defaultTunnel = appDataRepository.getPrimaryOrFirstTunnel() + when { + watcherState.isEthernetConditionMet() -> { + Timber.i("$autoTunnel - tunnel on on ethernet condition met") + if (watcherState.vpnState.isDown()) { + defaultTunnel?.let { + tunnelService.get().startTunnel(it) + } + } + } + + watcherState.isMobileDataConditionMet() -> { + Timber.i("$autoTunnel - tunnel on mobile data condition met") + val mobileDataTunnel = getMobileDataTunnel() + val tunnel = + mobileDataTunnel ?: defaultTunnel + if (watcherState.vpnState.isDown() || activeTunnel?.isMobileDataTunnel == false) { + tunnel?.let { + tunnelService.get().startTunnel(it) + } + } + } + + watcherState.isTunnelOffOnMobileDataConditionMet() -> { + Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off") + if (!watcherState.vpnState.isDown()) { + activeTunnel?.let { + tunnelService.get().stopTunnel(it) + } + } + } + + watcherState.isUntrustedWifiConditionMet() -> { + Timber.i("Untrusted wifi condition met") + if (activeTunnel == null || watcherState.isCurrentSSIDActiveTunnelNetwork() == false || + watcherState.vpnState.isDown() + ) { + Timber.i( + "$autoTunnel - tunnel on ssid not associated with current tunnel condition met", + ) + watcherState.getTunnelWithMatchingTunnelNetwork()?.let { + Timber.i("Found tunnel associated with this SSID, bringing tunnel up: ${it.name}") + if (watcherState.vpnState.isDown() || activeTunnel?.id != it.id) { tunnelService.get().startTunnel(it) } - } - } - - watcherState.isMobileDataConditionMet() -> { - Timber.i("$autoTunnel - tunnel on mobile data condition met") - val mobileDataTunnel = getMobileDataTunnel() - val tunnel = - 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()) { - activeTunnel?.let { - tunnelService.get().stopTunnel(it) - } - } - } - - watcherState.isUntrustedWifiConditionMet() -> { - Timber.i("Untrusted wifi condition met") - if (activeTunnel?.tunnelNetworks?.isMatchingToWildcardList(watcherState.currentNetworkSSID) == false || - activeTunnel == null || isTunnelDown() - ) { - Timber.i( - "$autoTunnel - tunnel on ssid not associated with current tunnel condition met", - ) - watcherState.tunnels.firstOrNull { it.tunnelNetworks.isMatchingToWildcardList(watcherState.currentNetworkSSID) }?.let { - Timber.i("Found tunnel associated with this SSID, bringing tunnel up: ${it.name}") - if (isTunnelDown() || activeTunnel?.id != it.id) { + } ?: suspend { + Timber.i("No tunnel associated with this SSID, using defaults") + val default = appDataRepository.getPrimaryOrFirstTunnel() + if (default?.name != tunnelService.get().name || watcherState.vpnState.isDown()) { + default?.let { tunnelService.get().startTunnel(it) } - } ?: suspend { - Timber.i("No tunnel associated with this SSID, using defaults") - val default = appDataRepository.getPrimaryOrFirstTunnel() - if (default?.name != tunnelService.get().name || isTunnelDown()) { - default?.let { - tunnelService.get().startTunnel(it) - } - } - }.invoke() - } + } + }.invoke() } + } - watcherState.isTrustedWifiConditionMet() -> { - Timber.i( - "$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off", - ) - if (!isTunnelDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) } - } + watcherState.isTrustedWifiConditionMet() -> { + Timber.i( + "$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off", + ) + if (!watcherState.vpnState.isDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) } + } - watcherState.isTunnelOffOnWifiConditionMet() -> { - Timber.i( - "$autoTunnel - tunnel off on wifi condition met, turning vpn off", - ) - if (!isTunnelDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) } - } + watcherState.isTunnelOffOnWifiConditionMet() -> { + Timber.i( + "$autoTunnel - tunnel off on wifi condition met, turning vpn off", + ) + if (!watcherState.vpnState.isDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) } + } - watcherState.isTunnelOffOnNoConnectivityMet() -> { - Timber.i( - "$autoTunnel - tunnel off on no connectivity met, turning vpn off", - ) - if (!isTunnelDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) } - } + watcherState.isTunnelOffOnNoConnectivityMet() -> { + Timber.i( + "$autoTunnel - tunnel off on no connectivity met, turning vpn off", + ) + if (!watcherState.vpnState.isDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) } + } - else -> { - Timber.i("$autoTunnel - no condition met") - } + else -> { + Timber.i("$autoTunnel - no condition met") } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/AutoTunnelState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/AutoTunnelState.kt index adb8391..a6b3a39 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/AutoTunnelState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/AutoTunnelState.kt @@ -1,10 +1,13 @@ package com.zaneschepke.wireguardautotunnel.service.foreground import com.zaneschepke.wireguardautotunnel.data.domain.Settings +import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig +import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList data class AutoTunnelState( + val vpnState: VpnState = VpnState(), val isWifiConnected: Boolean = false, val isEthernetConnected: Boolean = false, val isMobileDataConnected: Boolean = false, @@ -41,7 +44,7 @@ data class AutoTunnelState( return ( !isEthernetConnected && isWifiConnected && - !settings.trustedNetworkSSIDs.isMatchingToWildcardList(currentNetworkSSID) && + !isCurrentSSIDTrusted() && settings.isTunnelOnWifiEnabled ) } @@ -51,7 +54,7 @@ data class AutoTunnelState( !isEthernetConnected && ( isWifiConnected && - settings.trustedNetworkSSIDs.isMatchingToWildcardList(currentNetworkSSID) + isCurrentSSIDTrusted() ) ) } @@ -73,4 +76,32 @@ data class AutoTunnelState( !isMobileDataConnected ) } + + fun isCurrentSSIDTrusted(): Boolean { + return if (settings.isWildcardsEnabled) { + settings.trustedNetworkSSIDs.isMatchingToWildcardList(currentNetworkSSID) + } else { + settings.trustedNetworkSSIDs.contains(currentNetworkSSID) + } + } + fun isCurrentSSIDActiveTunnelNetwork(): Boolean { + val currentTunnelNetworks = vpnState.tunnelConfig?.tunnelNetworks + return ( + if (settings.isWildcardsEnabled) { + currentTunnelNetworks?.isMatchingToWildcardList(currentNetworkSSID) + } else { + currentTunnelNetworks?.contains(currentNetworkSSID) + } + ) == true + } + + fun getTunnelWithMatchingTunnelNetwork(): TunnelConfig? { + return tunnels.firstOrNull { + if (settings.isWildcardsEnabled) { + it.tunnelNetworks.isMatchingToWildcardList(currentNetworkSSID) + } else { + it.tunnelNetworks.contains(currentNetworkSSID) + } + } + } } 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 71939ea..08180c1 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,69 +3,81 @@ package com.zaneschepke.wireguardautotunnel.service.foreground import android.app.Service import android.content.Context import android.content.Intent -import android.net.VpnService +import com.zaneschepke.wireguardautotunnel.util.SingletonHolder +import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate +import jakarta.inject.Inject +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import timber.log.Timber -object ServiceManager { - private fun actionOnService(action: Action, context: Context, cls: Class, extras: Map? = null) { - if (VpnService.prepare(context) != null) return - val intent = - Intent(context, cls).also { - it.action = action.name - extras?.forEach { (k, v) -> it.putExtra(k, v) } - } - intent.component?.javaClass - try { - when (action) { - Action.START_FOREGROUND, Action.STOP_FOREGROUND -> - context.startForegroundService( - intent, - ) +@OptIn(ExperimentalCoroutinesApi::class) +class ServiceManager +@Inject constructor(private val context: Context) { - Action.START, Action.STOP -> context.startService(intent) - } - } catch (e: Exception) { - Timber.e(e.message) + private val _autoTunnelActive = MutableStateFlow(false) + + val autoTunnelActive = _autoTunnelActive.asStateFlow() + + var autoTunnelService = CompletableDeferred() + var backgroundService = CompletableDeferred() + + companion object : SingletonHolder(::ServiceManager) + + private fun startService(cls: Class, background: Boolean) { + val intent = Intent(context, cls) + if (background) { + context.startForegroundService(intent) + } else { + context.startService(intent) } } - fun startWatcherServiceForeground(context: Context) { - actionOnService( - Action.START_FOREGROUND, - context, - AutoTunnelService::class.java, - ) + suspend fun startAutoTunnel(background: Boolean) { + if (autoTunnelService.isCompleted) return _autoTunnelActive.update { true } + kotlin.runCatching { + startService(AutoTunnelService::class.java, background) + autoTunnelService.await() + autoTunnelService.getCompleted().start() + _autoTunnelActive.update { true } + }.onFailure { + Timber.e(it) + } } - fun startWatcherService(context: Context) { - actionOnService( - Action.START, - context, - AutoTunnelService::class.java, - ) + suspend fun startBackgroundService() { + if (backgroundService.isCompleted) return + kotlin.runCatching { + startService(TunnelBackgroundService::class.java, true) + backgroundService.await() + backgroundService.getCompleted().start() + }.onFailure { + Timber.e(it) + } } - fun stopWatcherService(context: Context) { - actionOnService( - Action.STOP, - context, - AutoTunnelService::class.java, - ) + fun stopBackgroundService() { + if (!backgroundService.isCompleted) return + runCatching { + backgroundService.getCompleted().stop() + }.onFailure { + Timber.e(it) + } } - fun startTunnelBackgroundService(context: Context) { - actionOnService( - Action.START_FOREGROUND, - context, - TunnelBackgroundService::class.java, - ) + fun stopAutoTunnel() { + if (!autoTunnelService.isCompleted) return + runCatching { + autoTunnelService.getCompleted().stop() + _autoTunnelActive.update { false } + }.onFailure { + Timber.e(it) + } } - fun stopTunnelBackgroundService(context: Context) { - actionOnService( - Action.STOP, - context, - TunnelBackgroundService::class.java, - ) + fun requestTunnelTileUpdate() { + context.requestTunnelTileServiceStateUpdate() } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/TunnelBackgroundService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/TunnelBackgroundService.kt index b59082f..66ae9c9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/TunnelBackgroundService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/TunnelBackgroundService.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.LifecycleService import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CompletableDeferred import javax.inject.Inject @AndroidEntryPoint @@ -15,6 +16,9 @@ class TunnelBackgroundService : LifecycleService() { @Inject lateinit var notificationService: NotificationService + @Inject + lateinit var serviceManager: ServiceManager + private val foregroundId = 123 override fun onCreate() { @@ -29,27 +33,24 @@ class TunnelBackgroundService : LifecycleService() { } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - if (intent != null) { - val action = intent.action - when (action) { - Action.START.name, - Action.START_FOREGROUND.name, - -> startService() - Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService() - } - } + serviceManager.backgroundService.complete(this) return super.onStartCommand(intent, flags, startId) } - private fun startService() { + fun start() { startForeground(foregroundId, createNotification()) } - private fun stopService() { + fun stop() { stopForeground(STOP_FOREGROUND_REMOVE) stopSelf() } + override fun onDestroy() { + serviceManager.backgroundService = CompletableDeferred() + super.onDestroy() + } + private fun createNotification(): Notification { return notificationService.createNotification( getString(R.string.vpn_channel_id), 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 d61ea71..7377a3a 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 @@ -6,9 +6,8 @@ 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.AutoTunnelService +import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager 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.CoroutineScope import kotlinx.coroutines.launch @@ -24,6 +23,9 @@ class ShortcutsActivity : ComponentActivity() { @Inject lateinit var tunnelService: Provider + @Inject + lateinit var serviceManager: ServiceManager + @Inject @ApplicationScope lateinit var applicationScope: CoroutineScope @@ -44,26 +46,16 @@ class ShortcutsActivity : ComponentActivity() { Timber.d("Shortcut action on name: ${tunnelConfig?.name}") tunnelConfig?.let { when (intent.action) { - Action.START.name -> this@ShortcutsActivity.startTunnelBackground(it.id) - Action.STOP.name -> this@ShortcutsActivity.stopTunnelBackground(it.id) + Action.START.name -> tunnelService.get().startTunnel(it, true) + Action.STOP.name -> tunnelService.get().stopTunnel(it) else -> Unit } } } AutoTunnelService::class.java.simpleName, LEGACY_AUTO_TUNNEL_SERVICE_NAME -> { when (intent.action) { - Action.START.name -> - appDataRepository.settings.save( - settings.copy( - isAutoTunnelPaused = false, - ), - ) - Action.STOP.name -> - appDataRepository.settings.save( - settings.copy( - isAutoTunnelPaused = true, - ), - ) + Action.START.name -> serviceManager.startAutoTunnel(true) + Action.STOP.name -> serviceManager.stopAutoTunnel() } } } 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 3bcd9db..0a445e3 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,7 +1,6 @@ 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 @@ -9,9 +8,9 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.lifecycleScope -import com.zaneschepke.wireguardautotunnel.R 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.CoroutineScope import kotlinx.coroutines.launch @@ -23,51 +22,18 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner { @Inject lateinit var appDataRepository: AppDataRepository + @Inject + lateinit var serviceManager: ServiceManager + @Inject @ApplicationScope lateinit var applicationScope: CoroutineScope private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this) - /* 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() - } - } - }.onFailure { - Timber.e(it) - } - } - } } override fun onStopListening() { @@ -82,26 +48,28 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner { override fun onStartListening() { super.onStartListening() lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) + lifecycleScope.launch { + if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable() + updateTileState() + } + } + + private fun updateTileState() { + serviceManager.autoTunnelActive.value.let { + if (it) setActive() else setInactive() + } } override fun onClick() { super.onClick() unlockAndRun { lifecycleScope.launch { - 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, - ), - ) + if (serviceManager.autoTunnelActive.value) { + serviceManager.stopAutoTunnel() + setInactive() + } else { + serviceManager.startAutoTunnel(true) + setActive() } } } @@ -128,16 +96,15 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner { } } - private fun setTileDescription(description: String) { - 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() + /* 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 (_: Throwable) { + Timber.e("Failed to bind to TunnelControlTile") } + return ret } override val lifecycle: Lifecycle 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 0da6a01..253247d 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 @@ -1,6 +1,8 @@ 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 @@ -11,8 +13,6 @@ 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.tunnel.TunnelService -import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground -import com.zaneschepke.wireguardautotunnel.util.extensions.stopTunnelBackground import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -36,7 +36,6 @@ class TunnelControlTile : TileService(), LifecycleOwner { override fun onCreate() { super.onCreate() - Timber.d("onCreate for tile service") lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) } @@ -52,6 +51,7 @@ class TunnelControlTile : TileService(), LifecycleOwner { override fun onStartListening() { super.onStartListening() lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) + Timber.d("Updating tile!") lifecycleScope.launch { if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable() updateTileState() @@ -60,6 +60,7 @@ class TunnelControlTile : TileService(), LifecycleOwner { private suspend fun updateTileState() { val lastActive = appDataRepository.getStartTunnelConfig() + Timber.d("Got config $lastActive") lastActive?.let { updateTile(it) } @@ -68,13 +69,15 @@ class TunnelControlTile : TileService(), LifecycleOwner { override fun onClick() { super.onClick() unlockAndRun { - Timber.d("Click") lifecycleScope.launch { - val context = this@TunnelControlTile val lastActive = appDataRepository.getStartTunnelConfig() lastActive?.let { tunnel -> - if (tunnel.isActive) return@launch context.stopTunnelBackground(tunnel.id) - context.startTunnelBackground(tunnel.id) + if (tunnel.isActive) { + tunnelService.get().stopTunnel(tunnel) + } else { + tunnelService.get().startTunnel(tunnel, true) + } + updateTileState() } } } @@ -124,6 +127,17 @@ class TunnelControlTile : TileService(), LifecycleOwner { } } + /* 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 (_: Throwable) { + Timber.e("Failed to bind to TunnelControlTile") + } + return ret + } + override val lifecycle: Lifecycle get() = lifecycleRegistry } 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 index d1279dc..41ebf7e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/TunnelService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/TunnelService.kt @@ -5,7 +5,7 @@ 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 startTunnel(tunnelConfig: TunnelConfig, background: Boolean = false): Result suspend fun stopTunnel(tunnelConfig: TunnelConfig): Result @@ -18,5 +18,6 @@ interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel { suspend fun getState(): TunnelState fun cancelStatsJob() + fun startStatsJob() } 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 a511d5d..16948ac 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 @@ -8,6 +8,7 @@ import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepositor import com.zaneschepke.wireguardautotunnel.module.ApplicationScope import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.module.Kernel +import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics @@ -36,6 +37,7 @@ constructor( private val appDataRepository: AppDataRepository, @ApplicationScope private val applicationScope: CoroutineScope, @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + private val serviceManager: ServiceManager, ) : TunnelService { private val _vpnState = MutableStateFlow(VpnState()) @@ -87,9 +89,9 @@ constructor( } } - override suspend fun startTunnel(tunnelConfig: TunnelConfig): Result { + override suspend fun startTunnel(tunnelConfig: TunnelConfig, background: Boolean): Result { return withContext(ioDispatcher) { - onBeforeStart(tunnelConfig) + onBeforeStart(tunnelConfig, background) setState(tunnelConfig, TunnelState.UP).onSuccess { emitTunnelState(it) }.onFailure { @@ -143,18 +145,28 @@ constructor( resetBackendStatistics() } - private suspend fun onBeforeStart(tunnelConfig: TunnelConfig) { - if (_vpnState.value.status == TunnelState.UP) vpnState.value.tunnelConfig?.let { stopTunnel(it) } + private suspend fun onBeforeStart(tunnelConfig: TunnelConfig, background: Boolean) { + if (_vpnState.value.status == TunnelState.UP && + tunnelConfig != _vpnState.value.tunnelConfig + ) { + vpnState.value.tunnelConfig?.let { stopTunnel(it) } + } + if (background) serviceManager.startBackgroundService() resetBackendStatistics() appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true)) emitVpnStateConfig(tunnelConfig) startStatsJob() + Timber.d("Updating start") + serviceManager.requestTunnelTileUpdate() } private suspend fun onBeforeStop(tunnelConfig: TunnelConfig) { cancelStatsJob() resetBackendStatistics() appDataRepository.tunnels.save(tunnelConfig.copy(isActive = false)) + serviceManager.stopBackgroundService() + Timber.d("UPdating stop") + serviceManager.requestTunnelTileUpdate() } private fun emitTunnelState(state: TunnelState) { 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 3c4a6be..ae7ab7e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppUiState.kt @@ -10,6 +10,5 @@ data class AppUiState( val tunnels: List = emptyList(), val vpnState: VpnState = VpnState(), val generalState: GeneralState = GeneralState(), - val isKernelAvailable: Boolean = false, - val isRooted: Boolean = false, + val autoTunnelActive: 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 468303a..f47149e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt @@ -2,8 +2,6 @@ package com.zaneschepke.wireguardautotunnel.ui 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.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.module.IoDispatcher @@ -11,7 +9,6 @@ import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState import com.zaneschepke.wireguardautotunnel.util.Constants -import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow @@ -20,13 +17,10 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.takeWhile -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.plus -import kotlinx.coroutines.withContext import xyz.teamgravity.pin_lock_compose.PinManager import javax.inject.Inject import javax.inject.Provider @@ -38,6 +32,7 @@ constructor( private val appDataRepository: AppDataRepository, private val tunnelService: Provider, @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + private val serviceManager: ServiceManager, ) : ViewModel() { val uiState = @@ -46,12 +41,14 @@ constructor( appDataRepository.tunnels.getTunnelConfigsFlow(), tunnelService.get().vpnState, appDataRepository.appState.generalStateFlow, - ) { settings, tunnels, tunnelState, generalState -> + serviceManager.autoTunnelActive, + ) { settings, tunnels, tunnelState, generalState, autoTunnel -> AppUiState( settings, tunnels, tunnelState, generalState, + autoTunnel, ) }.stateIn( viewModelScope + ioDispatcher, @@ -95,7 +92,7 @@ constructor( private suspend fun initAutoTunnel() { val settings = appDataRepository.settings.getSettings() - if (settings.isAutoTunnelEnabled) ServiceManager.startWatcherService(WireGuardAutoTunnel.instance) + if (settings.isAutoTunnelEnabled) serviceManager.startAutoTunnel(false) } fun onPinLockDisabled() = viewModelScope.launch(ioDispatcher) { 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 b8d1583..1e2cddb 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt @@ -39,9 +39,7 @@ import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.data.datastore.LocaleStorage import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository -import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService -import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalFocusRequester @@ -67,6 +65,7 @@ import com.zaneschepke.wireguardautotunnel.util.LocaleUtil import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate import dagger.hilt.android.AndroidEntryPoint +import timber.log.Timber import javax.inject.Inject @AndroidEntryPoint @@ -100,17 +99,17 @@ class MainActivity : AppCompatActivity() { val navController = rememberNavController() val rootItemFocusRequester = remember { FocusRequester() } - LaunchedEffect(appUiState.vpnState.status) { - val context = this@MainActivity - when (appUiState.vpnState.status) { - TunnelState.DOWN -> ServiceManager.stopTunnelBackgroundService(context) - else -> Unit - } - context.requestTunnelTileServiceStateUpdate() + LaunchedEffect(appUiState.tunnels) { + Timber.d("Updating launched") + requestTunnelTileServiceStateUpdate() + } + + LaunchedEffect(appUiState.autoTunnelActive) { + requestAutoTunnelTileServiceUpdate() } with(appUiState.settings) { - LaunchedEffect(isAutoTunnelPaused, isAutoTunnelEnabled) { + LaunchedEffect(isAutoTunnelEnabled) { this@MainActivity.requestAutoTunnelTileServiceUpdate() } } @@ -248,4 +247,3 @@ class MainActivity : AppCompatActivity() { tunnelService.cancelStatsJob() } } - diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt index 5cf74b2..16ba4f3 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt @@ -31,7 +31,7 @@ fun ClickableIconButton(onClick: () -> Unit, onIconClick: () -> Unit, text: Stri if (enabled) { onIconClick() } - }, + }, ) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ExpandingRowListItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ExpandingRowListItem.kt index 0109326..d0a1a41 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ExpandingRowListItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ExpandingRowListItem.kt @@ -16,8 +16,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/IconSurfaceButton.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/IconSurfaceButton.kt index 6ceb0c4..fefc063 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/IconSurfaceButton.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/IconSurfaceButton.kt @@ -2,7 +2,6 @@ package com.zaneschepke.wireguardautotunnel.ui.common.button import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.clickable -import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -31,21 +30,27 @@ import kotlin.let @androidx.compose.runtime.Composable fun IconSurfaceButton(title: String, onClick: () -> Unit, selected: Boolean, leadingIcon: ImageVector? = null, description: String? = null) { val border: BorderStroke? = - if (selected) BorderStroke( - 1.dp, - MaterialTheme.colorScheme.primary - ) else null - Card( - modifier = - Modifier - .fillMaxWidth() - .height(IntrinsicSize.Min), - shape = RoundedCornerShape(8.dp), - border = border, - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), - ) { - Box(modifier = Modifier.clickable { onClick() } - .fillMaxWidth()) { + if (selected) { + BorderStroke( + 1.dp, + MaterialTheme.colorScheme.primary, + ) + } else { + null + } + Card( + modifier = + Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min), + shape = RoundedCornerShape(8.dp), + border = border, + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + ) { + Box( + modifier = Modifier.clickable { onClick() } + .fillMaxWidth(), + ) { Column( modifier = Modifier @@ -61,7 +66,7 @@ fun IconSurfaceButton(title: String, onClick: () -> Unit, selected: Boolean, lea ) { Row( horizontalArrangement = Arrangement.spacedBy( - 16.dp.scaledWidth() + 16.dp.scaledWidth(), ), verticalAlignment = Alignment.Companion.CenterVertically, modifier = Modifier.padding(vertical = if (description == null) 10.dp.scaledHeight() else 0.dp), @@ -77,7 +82,7 @@ fun IconSurfaceButton(title: String, onClick: () -> Unit, selected: Boolean, lea Column { Text( title, - style = MaterialTheme.typography.titleMedium + style = MaterialTheme.typography.titleMedium, ) description?.let { Text( @@ -91,5 +96,5 @@ fun IconSurfaceButton(title: String, onClick: () -> Unit, selected: Boolean, lea } } } - } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SelectionItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SelectionItem.kt index 0260594..f92eec4 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SelectionItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SelectionItem.kt @@ -1,7 +1,6 @@ package com.zaneschepke.wireguardautotunnel.ui.common.button.surface import androidx.compose.runtime.Composable -import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.graphics.vector.ImageVector data class SelectionItem( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SurfaceSelectionGroupButton.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SurfaceSelectionGroupButton.kt index 6dcedbb..cdea66c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SurfaceSelectionGroupButton.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SurfaceSelectionGroupButton.kt @@ -24,65 +24,63 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth @Composable fun SurfaceSelectionGroupButton(items: List) { - - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(8.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), - ) { - items.mapIndexed { index, item -> - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .then(item.onClick?.let { Modifier.clickable { it() }} ?: Modifier) - .fillMaxWidth() + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(8.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + ) { + items.mapIndexed { index, item -> + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .then(item.onClick?.let { Modifier.clickable { it() } } ?: Modifier) + .fillMaxWidth(), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()), ) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()), + modifier = Modifier + .padding(start = 16.dp.scaledWidth()) + .weight(4f, false) + .fillMaxWidth(), ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .padding(start = 16.dp.scaledWidth()) - .weight(4f, false) - .fillMaxWidth(), - ) { - item.leadingIcon?.let { icon -> - Icon( - icon, - icon.name, - modifier = Modifier.size(iconSize), - ) - } - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically), - modifier = Modifier - .fillMaxWidth() - .padding(start = if (item.leadingIcon != null) 16.dp.scaledWidth() else 0.dp) - .padding(vertical = if (item.description == null) 16.dp.scaledHeight() else 6.dp.scaledHeight()), - ) { - item.title() - item.description?.let { - it() - } - } + item.leadingIcon?.let { icon -> + Icon( + icon, + icon.name, + modifier = Modifier.size(iconSize), + ) } - item.trailing?.let { - Box( - contentAlignment = Alignment.CenterEnd, - modifier = Modifier - .padding(end = 24.dp.scaledWidth(), start = 16.dp.scaledWidth()) - .weight(1f), - ) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically), + modifier = Modifier + .fillMaxWidth() + .padding(start = if (item.leadingIcon != null) 16.dp.scaledWidth() else 0.dp) + .padding(vertical = if (item.description == null) 16.dp.scaledHeight() else 6.dp.scaledHeight()), + ) { + item.title() + item.description?.let { it() } } } + item.trailing?.let { + Box( + contentAlignment = Alignment.CenterEnd, + modifier = Modifier + .padding(end = 24.dp.scaledWidth(), start = 16.dp.scaledWidth()) + .weight(1f), + ) { + it() + } + } } - if (index + 1 != items.size) HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) } + if (index + 1 != items.size) HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) } } - +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/SubmitConfigurationTextBox.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/SubmitConfigurationTextBox.kt index bb78fe2..ce23ed9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/SubmitConfigurationTextBox.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/SubmitConfigurationTextBox.kt @@ -18,8 +18,6 @@ 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.focusRequester import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/label/GroupLabel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/label/GroupLabel.kt index fd2eea4..59fc2ca 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/label/GroupLabel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/label/GroupLabel.kt @@ -19,4 +19,3 @@ fun GroupLabel(title: String) { ) } } - diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/label/VersionLabel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/label/VersionLabel.kt index 16051a7..ba7702b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/label/VersionLabel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/label/VersionLabel.kt @@ -27,7 +27,7 @@ fun VersionLabel() { color = MaterialTheme.colorScheme.outline, modifier = Modifier.clickable { clipboardManager.setText(AnnotatedString(BuildConfig.VERSION_NAME)) - } + }, ) } } 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 4155b95..b9f6cb1 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 @@ -8,21 +8,11 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.KeyEventType -import androidx.compose.ui.input.key.key -import androidx.compose.ui.input.key.onKeyEvent -import androidx.compose.ui.input.key.type import androidx.navigation.NavController import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.compose.currentBackStackEntryAsState -import timber.log.Timber @Composable fun BottomNavBar(navController: NavController, bottomNavItems: List) { @@ -34,7 +24,6 @@ fun BottomNavBar(navController: NavController, bottomNavItems: List { } val LocalFocusRequester = compositionLocalOf { error("FocusRequester is not provided") } - diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/TopNavBar.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/TopNavBar.kt index 9feb69b..494ee5f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/TopNavBar.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/TopNavBar.kt @@ -18,12 +18,14 @@ fun TopNavBar(title: String, trailing: @Composable () -> Unit = {}, showBack: Bo Text(title) }, navigationIcon = { - if(showBack) IconButton(onClick = { navController.popBackStack() }) { - val icon = Icons.AutoMirrored.Outlined.ArrowBack - Icon( - imageVector = icon, - contentDescription = icon.name, - ) + if (showBack) { + IconButton(onClick = { navController.popBackStack() }) { + val icon = Icons.AutoMirrored.Outlined.ArrowBack + Icon( + imageVector = icon, + contentDescription = icon.name, + ) + } } }, actions = { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt index a2a859f..2a6a10c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt @@ -234,7 +234,7 @@ fun ConfigScreen(tunnelId: Int) { hint = stringResource(R.string.tunnel_name).lowercase(), modifier = Modifier - .fillMaxWidth() + .fillMaxWidth(), ) OutlinedTextField( modifier = @@ -347,7 +347,7 @@ fun ConfigScreen(tunnelId: Int) { hint = stringResource(R.string.junk_packet_count).lowercase(), modifier = Modifier - .fillMaxWidth() + .fillMaxWidth(), ) ConfigurationTextBox( value = uiState.interfaceProxy.junkPacketMinSize, @@ -360,7 +360,7 @@ fun ConfigScreen(tunnelId: Int) { ).lowercase(), modifier = Modifier - .fillMaxWidth() + .fillMaxWidth(), ) ConfigurationTextBox( value = uiState.interfaceProxy.junkPacketMaxSize, @@ -373,7 +373,7 @@ fun ConfigScreen(tunnelId: Int) { ).lowercase(), modifier = Modifier - .fillMaxWidth() + .fillMaxWidth(), ) ConfigurationTextBox( value = uiState.interfaceProxy.initPacketJunkSize, @@ -383,7 +383,7 @@ fun ConfigScreen(tunnelId: Int) { hint = stringResource(R.string.init_packet_junk_size).lowercase(), modifier = Modifier - .fillMaxWidth() + .fillMaxWidth(), ) ConfigurationTextBox( value = uiState.interfaceProxy.responsePacketJunkSize, @@ -396,7 +396,7 @@ fun ConfigScreen(tunnelId: Int) { ).lowercase(), modifier = Modifier - .fillMaxWidth() + .fillMaxWidth(), ) ConfigurationTextBox( value = uiState.interfaceProxy.initPacketMagicHeader, @@ -409,7 +409,7 @@ fun ConfigScreen(tunnelId: Int) { ).lowercase(), modifier = Modifier - .fillMaxWidth() + .fillMaxWidth(), ) ConfigurationTextBox( value = uiState.interfaceProxy.responsePacketMagicHeader, @@ -422,7 +422,7 @@ fun ConfigScreen(tunnelId: Int) { ).lowercase(), modifier = Modifier - .fillMaxWidth() + .fillMaxWidth(), ) ConfigurationTextBox( value = uiState.interfaceProxy.underloadPacketMagicHeader, @@ -435,7 +435,7 @@ fun ConfigScreen(tunnelId: Int) { ).lowercase(), modifier = Modifier - .fillMaxWidth() + .fillMaxWidth(), ) ConfigurationTextBox( value = uiState.interfaceProxy.transportPacketMagicHeader, @@ -448,7 +448,7 @@ fun ConfigScreen(tunnelId: Int) { ).lowercase(), modifier = Modifier - .fillMaxWidth() + .fillMaxWidth(), ) } Row( 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 db40bfd..eeffcc8 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,7 +1,11 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main +import android.content.Intent +import android.net.Uri import android.net.VpnService +import android.provider.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.foundation.ExperimentalFoundationApi @@ -53,9 +57,9 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelImpo import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelRowItem import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.VpnDeniedDialog import com.zaneschepke.wireguardautotunnel.util.Constants +import com.zaneschepke.wireguardautotunnel.util.extensions.isBatteryOptimizationsDisabled import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl -import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground @OptIn(ExperimentalFoundationApi::class) @Composable @@ -75,13 +79,19 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState) NestedScrollListener({ isFabVisible = false }, { isFabVisible = true }) } - val vpnActivityResultState = + val vpnActivity = rememberLauncherForActivityResult( ActivityResultContracts.StartActivityForResult(), onResult = { if (it.resultCode != RESULT_OK) showVpnPermissionDialog = true }, ) + val batteryActivity = + rememberLauncherForActivityResult( + ActivityResultContracts.StartActivityForResult(), + ) { result: ActivityResult -> + viewModel.setBatteryOptimizeDisableShown() + } val tunnelFileImportResultLauncher = rememberFileImportLauncherForResult(onNoFileExplorer = { snackbar.showMessage( @@ -104,7 +114,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState) InfoDialog( onDismiss = { showDeleteTunnelAlertDialog = false }, onAttest = { - selectedTunnel?.let { viewModel.onDelete(it, context) } + selectedTunnel?.let { viewModel::onDelete } showDeleteTunnelAlertDialog = false selectedTunnel = null }, @@ -114,15 +124,35 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState) ) } + fun requestBatteryOptimizationsDisabled() { + val intent = + Intent().apply { + action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS + data = Uri.parse("package:${context.packageName}") + } + batteryActivity.launch(intent) + } + + fun onAutoTunnelToggle() { + if (!uiState.generalState.isBatteryOptimizationDisableShown && + !context.isBatteryOptimizationsDisabled() && !isRunningOnTv + ) { + return requestBatteryOptimizationsDisabled() + } + val intent = if (!uiState.settings.isKernelEnabled) { + VpnService.prepare(context) + } else { + null + } + if (intent != null) return vpnActivity.launch(intent) + viewModel.onToggleAutoTunnel() + } + fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) { val intent = if (uiState.settings.isKernelEnabled) null else VpnService.prepare(context) - if (intent != null) return vpnActivityResultState.launch(intent) + if (intent != null) return vpnActivity.launch(intent) if (!checked) viewModel.onTunnelStop(tunnel).also { return } - if (uiState.settings.isKernelEnabled) { - context.startTunnelBackground(tunnel.id) - } else { - viewModel.onTunnelStart(tunnel) - } + viewModel.onTunnelStart(tunnel, uiState.settings.isKernelEnabled) } Scaffold( @@ -137,34 +167,38 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState) }, floatingActionButtonPosition = FabPosition.End, floatingActionButton = { - if(!isRunningOnTv) ScrollDismissFab({ - val icon = Icons.Filled.Add - Icon( - imageVector = icon, - contentDescription = icon.name, - tint = MaterialTheme.colorScheme.onPrimary, - ) - }, isVisible = isFabVisible, onClick = { - showBottomSheet = true - }) + if (!isRunningOnTv) { + ScrollDismissFab({ + val icon = Icons.Filled.Add + Icon( + imageVector = icon, + contentDescription = icon.name, + tint = MaterialTheme.colorScheme.onPrimary, + ) + }, isVisible = isFabVisible, onClick = { + showBottomSheet = true + }) + } }, topBar = { - if(isRunningOnTv) TopNavBar( - showBack = false, - title = stringResource(R.string.app_name), - trailing = { - IconButton(onClick = { - showBottomSheet = true - }) { - val icon = Icons.Outlined.Add - Icon( - imageVector = icon, - contentDescription = icon.name, - ) - } - } - ) - } + if (isRunningOnTv) { + TopNavBar( + showBack = false, + title = stringResource(R.string.app_name), + trailing = { + IconButton(onClick = { + showBottomSheet = true + }) { + val icon = Icons.Outlined.Add + Icon( + imageVector = icon, + contentDescription = icon.name, + ) + } + }, + ) + } + }, ) { TunnelImportSheet( showBottomSheet, @@ -196,7 +230,9 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState) } } else { item { - AutoTunnelRowItem(uiState.settings, { viewModel.onToggleAutoTunnel(context) }) + AutoTunnelRowItem(uiState, { + onAutoTunnelToggle() + }) } } items( 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 5454e47..bd3d24b 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 @@ -30,26 +30,24 @@ import timber.log.Timber import java.io.InputStream import java.util.zip.ZipInputStream import javax.inject.Inject +import javax.inject.Provider @HiltViewModel class MainViewModel @Inject constructor( private val appDataRepository: AppDataRepository, - val tunnelService: TunnelService, + private val tunnelService: Provider, @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + private val serviceManager: ServiceManager, ) : ViewModel() { - private fun stopWatcherService(context: Context) { - ServiceManager.stopWatcherService(context) - } - - fun onDelete(tunnel: TunnelConfig, context: Context) { + fun onDelete(tunnel: TunnelConfig) { viewModelScope.launch { val settings = appDataRepository.settings.getSettings() val isPrimary = tunnel.isPrimaryTunnel if (appDataRepository.tunnels.count() == 1 || isPrimary) { - stopWatcherService(context) + serviceManager.stopAutoTunnel() resetTunnelSetting(settings) } appDataRepository.tunnels.delete(tunnel) @@ -69,14 +67,14 @@ constructor( appDataRepository.appState.setTunnelStatsExpanded(expanded) } - fun onTunnelStart(tunnelConfig: TunnelConfig) = viewModelScope.launch { + fun onTunnelStart(tunnelConfig: TunnelConfig, background: Boolean) = viewModelScope.launch { Timber.i("Starting tunnel ${tunnelConfig.name}") - tunnelService.startTunnel(tunnelConfig) + tunnelService.get().startTunnel(tunnelConfig, background) } fun onTunnelStop(tunnel: TunnelConfig) = viewModelScope.launch { Timber.i("Stopping active tunnel") - tunnelService.stopTunnel(tunnel) + tunnelService.get().stopTunnel(tunnel) } private fun generateQrCodeDefaultName(config: String): String { @@ -160,16 +158,17 @@ constructor( } } - fun onToggleAutoTunnel(context: Context) = viewModelScope.launch { + fun onToggleAutoTunnel() = viewModelScope.launch { val settings = appDataRepository.settings.getSettings() - if (settings.isAutoTunnelEnabled) { - ServiceManager.stopWatcherService(context) + val toggled = !settings.isAutoTunnelEnabled + if (toggled) { + serviceManager.startAutoTunnel(false) } else { - ServiceManager.startWatcherService(context) + serviceManager.stopAutoTunnel() } appDataRepository.settings.save( settings.copy( - isAutoTunnelEnabled = !settings.isAutoTunnelEnabled, + isAutoTunnelEnabled = toggled, ), ) } @@ -195,6 +194,10 @@ constructor( } } + fun setBatteryOptimizeDisableShown() = viewModelScope.launch { + appDataRepository.appState.setBatteryOptimizationDisableShown(true) + } + private suspend fun saveTunnelFromConfUri(name: String, uri: Uri, context: Context) { val stream = getInputStreamFromUri(uri, context) ?: throw FileReadException saveTunnelConfigFromStream(stream, name) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/AutoTunnelRowItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/AutoTunnelRowItem.kt index bf2fe1e..efff234 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/AutoTunnelRowItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/AutoTunnelRowItem.kt @@ -4,30 +4,25 @@ import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Bolt import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale 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.res.stringResource -import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.unit.dp import com.zaneschepke.wireguardautotunnel.R -import com.zaneschepke.wireguardautotunnel.data.domain.Settings +import com.zaneschepke.wireguardautotunnel.ui.AppUiState import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree -import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight @Composable -fun AutoTunnelRowItem(settings: Settings, onToggle: () -> Unit) { +fun AutoTunnelRowItem(appUiState: AppUiState, onToggle: () -> Unit) { val context = LocalContext.current val itemFocusRequester = remember { FocusRequester() } ExpandingRowListItem( @@ -40,7 +35,7 @@ fun AutoTunnelRowItem(settings: Settings, onToggle: () -> Unit) { Modifier .size(16.dp.scaledHeight()).scale(1.5f), tint = - if (!settings.isAutoTunnelEnabled) { + if (!appUiState.autoTunnelActive) { Color.Gray } else { SilverTree @@ -50,10 +45,10 @@ fun AutoTunnelRowItem(settings: Settings, onToggle: () -> Unit) { text = stringResource(R.string.auto_tunneling), trailing = { ScaledSwitch( - settings.isAutoTunnelEnabled, + appUiState.settings.isAutoTunnelEnabled, onClick = { onToggle() - } + }, ) }, onClick = { 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 index dc750fd..b2cf6c9 100644 --- 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 @@ -9,8 +9,6 @@ import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.MaterialTheme 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.unit.dp @Composable diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelRowItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelRowItem.kt index 35011ff..82d8f55 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelRowItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelRowItem.kt @@ -182,14 +182,14 @@ fun TunnelRowItem( ScaledSwitch( modifier = Modifier.focusRequester(itemFocusRequester), checked = isActive, - onClick = onSwitchClick + onClick = onSwitchClick, ) } } else { ScaledSwitch( modifier = Modifier.focusRequester(itemFocusRequester), checked = isActive, - onClick = onSwitchClick + onClick = onSwitchClick, ) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelStatisticsRow.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelStatisticsRow.kt index c07fdc4..1d06ec7 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelStatisticsRow.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelStatisticsRow.kt @@ -46,14 +46,18 @@ fun TunnelStatisticsRow(statistics: TunnelStatistics?, tunnelConfig: TunnelConfi Column( verticalArrangement = Arrangement.spacedBy(10.dp), ) { - Text(stringResource(R.string.peer).lowercase() + ": $peerId", style = MaterialTheme.typography.bodySmall) - Text("tx: $peerTxMB MB", style = MaterialTheme.typography.bodySmall) + Text( + stringResource(R.string.peer).lowercase() + ": $peerId", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.outline, + ) + Text("tx: $peerTxMB MB", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline) } Column( verticalArrangement = Arrangement.spacedBy(10.dp), ) { - Text(stringResource(R.string.handshake) + ": $handshake", style = MaterialTheme.typography.bodySmall) - Text("rx: $peerRxMB MB", style = MaterialTheme.typography.bodySmall) + Text(stringResource(R.string.handshake) + ": $handshake", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline) + Text("rx: $peerRxMB MB", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline) } } } 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 d60c2db..c79c90d 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 @@ -4,15 +4,10 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.WindowInsets 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.layout.systemBars -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Edit import androidx.compose.material.icons.outlined.NetworkPing @@ -27,18 +22,11 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.KeyEventType -import androidx.compose.ui.input.key.key -import androidx.compose.ui.input.key.onKeyEvent -import androidx.compose.ui.input.key.type import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -56,7 +44,6 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.compon import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth -import kotlinx.coroutines.delay @OptIn(ExperimentalLayoutApi::class) @Composable @@ -84,7 +71,7 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiSta ) } }) - } + }, ) { Column( horizontalAlignment = Alignment.Start, @@ -101,7 +88,12 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiSta listOf( SelectionItem( Icons.Outlined.Star, - title = { Text(stringResource(R.string.primary_tunnel), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, + title = { + Text( + stringResource(R.string.primary_tunnel), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, description = { Text( stringResource(R.string.set_primary_tunnel), @@ -114,7 +106,7 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiSta onClick = { optionsViewModel.onTogglePrimaryTunnel(config) }, ) }, - onClick = { optionsViewModel.onTogglePrimaryTunnel(config) } + onClick = { optionsViewModel.onTogglePrimaryTunnel(config) }, ), SelectionItem( Icons.Outlined.PhoneAndroid, @@ -131,7 +123,7 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiSta onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) }, ) }, - onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) } + onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) }, ), SelectionItem( Icons.Outlined.NetworkPing, @@ -147,7 +139,7 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiSta onClick = { optionsViewModel.onToggleRestartOnPing(config) }, ) }, - onClick = { optionsViewModel.onToggleRestartOnPing(config) } + onClick = { optionsViewModel.onToggleRestartOnPing(config) }, ), SelectionItem( title = { @@ -180,24 +172,25 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiSta style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), ) } - } } - }, description = { TrustedNetworkTextBox( - config.tunnelNetworks, onDelete = { optionsViewModel.onDeleteRunSSID(it, config) }, + config.tunnelNetworks, + onDelete = { optionsViewModel.onDeleteRunSSID(it, config) }, currentText = currentText, onSave = { optionsViewModel.onSaveRunSSID(it, config) }, onValueChange = { currentText = it }, - supporting = { if(appUiState.generalState.isWildcardsEnabled) { - WildcardsLabel() - }} + supporting = { + if (appUiState.settings.isWildcardsEnabled) { + WildcardsLabel() + } + }, ) }, - ) - ) + ), + ), ) } } 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 d7d3236..6fc4f96 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 @@ -32,7 +32,7 @@ constructor( } fun onSaveRunSSID(ssid: String, tunnelConfig: TunnelConfig) = viewModelScope.launch { - if(ssid.isBlank()) return@launch + if (ssid.isBlank()) return@launch val trimmed = ssid.trim() val tunnelsWithName = appDataRepository.tunnels.findByTunnelNetworksName(trimmed) 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 09a4067..08027f7 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 @@ -1,26 +1,13 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings -import android.content.Context.POWER_SERVICE -import android.content.Intent -import android.net.Uri -import android.net.VpnService -import android.os.PowerManager -import android.provider.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.foundation.clickable import androidx.compose.foundation.focusable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -49,7 +36,6 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState @@ -63,13 +49,9 @@ import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalFocusReques import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController -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.ForwardButton -import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LocationServicesDialog import com.zaneschepke.wireguardautotunnel.ui.theme.topPadding import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv -import com.zaneschepke.wireguardautotunnel.util.extensions.launchAppSettings import com.zaneschepke.wireguardautotunnel.util.extensions.launchNotificationSettings import com.zaneschepke.wireguardautotunnel.util.extensions.launchVpnSettings import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight @@ -91,114 +73,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: val isRunningOnTv = remember { context.isRunningOnTv() } val interactionSource = remember { MutableInteractionSource() } - - val settingsUiState by viewModel.uiState.collectAsStateWithLifecycle() - - var showVpnPermissionDialog by remember { mutableStateOf(false) } - var showLocationServicesAlertDialog by remember { mutableStateOf(false) } var showAuthPrompt by remember { mutableStateOf(false) } - var showLocationDialog by remember { mutableStateOf(false) } - - val startForResult = - rememberLauncherForActivityResult( - ActivityResultContracts.StartActivityForResult(), - ) { result: ActivityResult -> - if (result.resultCode == RESULT_OK) { - result.data - // Handle the Intent - } - viewModel.setBatteryOptimizeDisableShown() - } - - val vpnActivityResultState = - rememberLauncherForActivityResult( - ActivityResultContracts.StartActivityForResult(), - onResult = { - val accepted = (it.resultCode == RESULT_OK) - if (!accepted) { - showVpnPermissionDialog = true - } - }, - ) - - fun isBatteryOptimizationsDisabled(): Boolean { - val pm = context.getSystemService(POWER_SERVICE) as PowerManager - return pm.isIgnoringBatteryOptimizations(context.packageName) - } - - fun requestBatteryOptimizationsDisabled() { - val intent = - Intent().apply { - action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS - data = Uri.parse("package:${context.packageName}") - } - startForResult.launch(intent) - } - -// fun handleAutoTunnelToggle() { -// if (!uiState.generalState.isBatteryOptimizationDisableShown && -// !isBatteryOptimizationsDisabled() && !isRunningOnTv -// ) { -// return requestBatteryOptimizationsDisabled() -// } -// val intent = if (!uiState.settings.isKernelEnabled) { -// VpnService.prepare(context) -// } else { -// null -// } -// if (intent != null) return vpnActivityResultState.launch(intent) -// viewModel.onToggleAutoTunnel(context) -// } - - - - -// fun checkFineLocationGranted() { -// isBackgroundLocationGranted = -// if (!fineLocationState.status.isGranted) { -// false -// } else { -// viewModel.setLocationDisclosureShown() -// true -// } -// } - -// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { -// if ( -// isRunningOnTv && -// Build.VERSION.SDK_INT == Build.VERSION_CODES.Q -// ) { -// checkFineLocationGranted() -// } else { -// val backgroundLocationState = -// rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION) -// isBackgroundLocationGranted = -// if (!backgroundLocationState.status.isGranted) { -// false -// } else { -// SideEffect { viewModel.setLocationDisclosureShown() } -// true -// } -// } -// } -// -// if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { -// checkFineLocationGranted() -// } - - BackgroundLocationDialog( - showLocationDialog, - onDismiss = { showLocationDialog = false }, - onAttest = { showLocationDialog = false }, - ) - -// LocationServicesDialog( -// showLocationServicesAlertDialog, -// onDismiss = { showVpnPermissionDialog = false }, -// onAttest = { handleAutoTunnelToggle() }, -// ) - - VpnDeniedDialog(showVpnPermissionDialog, onDismiss = { showVpnPermissionDialog = false }) if (showAuthPrompt) { AuthorizationPrompt( @@ -231,12 +106,18 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: .padding(top = topPadding) .padding(bottom = 40.dp.scaledHeight()) .padding(horizontal = 24.dp.scaledWidth()) - .then(if(!isRunningOnTv) Modifier.clickable( - indication = null, - interactionSource = interactionSource, - ) { - focusManager.clearFocus() - } else Modifier) + .then( + if (!isRunningOnTv) { + Modifier.clickable( + indication = null, + interactionSource = interactionSource, + ) { + focusManager.clearFocus() + } + } else { + Modifier + }, + ), ) { SurfaceSelectionGroupButton( listOf( @@ -250,73 +131,78 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: ) }, onClick = { - if(!uiState.generalState.isLocationDisclosureShown) return@SelectionItem navController.navigate(Route.LocationDisclosure) + if (!uiState.generalState.isLocationDisclosureShown) return@SelectionItem navController.navigate(Route.LocationDisclosure) navController.navigate(Route.AutoTunnel) }, trailing = { ForwardButton(Modifier.focusable().focusRequester(rootFocusRequester)) { navController.navigate(Route.AutoTunnel) } }, - ) - ) + ), + ), ) SurfaceSelectionGroupButton( buildList { - if (!isRunningOnTv) addAll( - listOf( - SelectionItem( - Icons.Filled.AppShortcut, - { - ScaledSwitch( - uiState.settings.isShortcutsEnabled, - onClick = { viewModel.onToggleShortcutsEnabled() }, - ) - }, - title = { - Text( - stringResource(R.string.enabled_app_shortcuts), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) - }, - onClick = { viewModel.onToggleShortcutsEnabled() } + if (!isRunningOnTv) { + addAll( + listOf( + SelectionItem( + Icons.Filled.AppShortcut, + { + ScaledSwitch( + uiState.settings.isShortcutsEnabled, + onClick = { viewModel.onToggleShortcutsEnabled() }, + ) + }, + title = { + Text( + stringResource(R.string.enabled_app_shortcuts), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + onClick = { viewModel.onToggleShortcutsEnabled() }, + ), + SelectionItem( + Icons.Outlined.VpnLock, + { + ScaledSwitch( + enabled = !( + ( + uiState.settings.isTunnelOnWifiEnabled || + uiState.settings.isTunnelOnEthernetEnabled || + uiState.settings.isTunnelOnMobileDataEnabled + ) && + uiState.settings.isAutoTunnelEnabled + ), + onClick = { viewModel.onToggleAlwaysOnVPN() }, + checked = uiState.settings.isAlwaysOnVpnEnabled, + ) + }, + title = { + Text( + stringResource(R.string.always_on_vpn_support), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + onClick = { viewModel.onToggleAlwaysOnVPN() }, + ), + SelectionItem( + Icons.Outlined.AdminPanelSettings, + title = { + Text( + stringResource(R.string.kill_switch), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + onClick = { + context.launchVpnSettings() + }, + trailing = { + ForwardButton { context.launchVpnSettings() } + }, + ), ), - SelectionItem( - Icons.Outlined.VpnLock, - { - ScaledSwitch( - enabled = !( - ( - uiState.settings.isTunnelOnWifiEnabled || - uiState.settings.isTunnelOnEthernetEnabled || - uiState.settings.isTunnelOnMobileDataEnabled - ) && - uiState.settings.isAutoTunnelEnabled - ), - onClick = { viewModel.onToggleAlwaysOnVPN() }, - checked = uiState.settings.isAlwaysOnVpnEnabled, - ) - }, - title = { - Text( - stringResource(R.string.always_on_vpn_support), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) - }, - onClick = { viewModel.onToggleAlwaysOnVPN() } - ), - SelectionItem( - Icons.Outlined.AdminPanelSettings, - title = { - Text( - stringResource(R.string.kill_switch), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) - }, - onClick = { - context.launchVpnSettings() - }, - trailing = { - ForwardButton { context.launchVpnSettings() } - }, - ) ) - ) + } add( SelectionItem( Icons.Outlined.Restore, @@ -329,25 +215,27 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: title = { Text( stringResource(R.string.restart_at_boot), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) }, - onClick = { viewModel.onToggleRestartAtBoot() } - ) + onClick = { viewModel.onToggleRestartAtBoot() }, + ), ) - } + }, ) SurfaceSelectionGroupButton( - listOf(SelectionItem( - Icons.AutoMirrored.Outlined.ViewQuilt, - title = { Text(stringResource(R.string.appearance), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, - onClick = { - navController.navigate(Route.Appearance) - }, - trailing = { - ForwardButton { navController.navigate(Route.Appearance) } - }, - ), + listOf( + SelectionItem( + Icons.AutoMirrored.Outlined.ViewQuilt, + title = { Text(stringResource(R.string.appearance), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, + onClick = { + navController.navigate(Route.Appearance) + }, + trailing = { + ForwardButton { navController.navigate(Route.Appearance) } + }, + ), SelectionItem( Icons.Outlined.Notifications, title = { Text(stringResource(R.string.notifications), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, @@ -360,7 +248,12 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: ), SelectionItem( Icons.Outlined.Pin, - title = { Text(stringResource(R.string.enable_app_lock), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, + title = { + Text( + stringResource(R.string.enable_app_lock), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, trailing = { ScaledSwitch( uiState.generalState.isPinLockEnabled, @@ -374,370 +267,67 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: }, ) }, - onClick = { if (uiState.generalState.isPinLockEnabled) { - appViewModel.onPinLockDisabled() - } else { - PinManager.initialize(context) - navController.navigate(Route.Lock) - } } - ) - )) - - if(!isRunningOnTv) SurfaceSelectionGroupButton(listOf( - SelectionItem( - Icons.Outlined.Code, - title = { Text(stringResource(R.string.kernel), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, - description = { - Text( - stringResource(R.string.use_kernel), - style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline), - ) - }, - trailing = { - ScaledSwitch( - uiState.settings.isKernelEnabled, - onClick = { viewModel.onToggleKernelMode() }, - enabled = !( - uiState.settings.isAutoTunnelEnabled || - uiState.settings.isAlwaysOnVpnEnabled || - (uiState.vpnState.status == TunnelState.UP) || - !settingsUiState.isKernelAvailable - ), - ) - }, - onClick = { - viewModel.onToggleKernelMode() - } - ), - )) - - if(!isRunningOnTv) SurfaceSelectionGroupButton( - listOf( - SelectionItem( - Icons.Outlined.FolderZip, - title = { Text(stringResource(R.string.export_configs), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, onClick = { - if (uiState.tunnels.isEmpty()) return@SelectionItem context.showToast(R.string.tunnel_required) - showAuthPrompt = true + if (uiState.generalState.isPinLockEnabled) { + appViewModel.onPinLockDisabled() + } else { + PinManager.initialize(context) + navController.navigate(Route.Lock) + } }, ), - ) + ), ) + if (!isRunningOnTv) { + SurfaceSelectionGroupButton( + listOf( + SelectionItem( + Icons.Outlined.Code, + title = { Text(stringResource(R.string.kernel), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, + description = { + Text( + stringResource(R.string.use_kernel), + style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline), + ) + }, + trailing = { + ScaledSwitch( + uiState.settings.isKernelEnabled, + onClick = { viewModel.onToggleKernelMode() }, + enabled = !( + uiState.settings.isAutoTunnelEnabled || + uiState.settings.isAlwaysOnVpnEnabled || + (uiState.vpnState.status == TunnelState.UP) + ), + ) + }, + onClick = { + viewModel.onToggleKernelMode() + }, + ), + ), + ) + } - - - -// Surface( -// tonalElevation = 2.dp, -// shadowElevation = 2.dp, -// shape = RoundedCornerShape(12.dp), -// color = MaterialTheme.colorScheme.surface, -// modifier = -// ( -// if (isRunningOnTv) { -// Modifier -// .height(IntrinsicSize.Min) -// .fillMaxWidth(fillMaxWidth) -// .padding(top = 10.dp) -// } else { -// Modifier -// .fillMaxWidth(fillMaxWidth) -// .padding(top = 20.dp) -// } -// ) -// .padding(bottom = 10.dp), -// ) { -// Column( -// horizontalAlignment = Alignment.Start, -// verticalArrangement = Arrangement.Top, -// modifier = Modifier.padding(15.dp), -// ) { -// SectionTitle( -// title = stringResource(id = R.string.auto_tunneling), -// padding = screenPadding, -// ) -// ConfigurationToggle( -// stringResource(id = R.string.tunnel_on_wifi), -// enabled = !uiState.settings.isAlwaysOnVpnEnabled, -// checked = uiState.settings.isTunnelOnWifiEnabled, -// onCheckChanged = { checked -> -// if (!checked || settingsUiState.isRooted) viewModel.onToggleTunnelOnWifi().also { return@ConfigurationToggle } -// onAutoTunnelWifiChecked() -// }, -// modifier = -// if (uiState.settings.isAutoTunnelEnabled) { -// Modifier -// } else { -// Modifier -// .focusRequester(focusRequester) -// }, -// ) -// if (uiState.settings.isTunnelOnWifiEnabled) { -// Column { -// FlowRow( -// modifier = -// Modifier -// .padding(screenPadding) -// .fillMaxWidth(), -// horizontalArrangement = Arrangement.spacedBy(5.dp), -// ) { -// uiState.settings.trustedNetworkSSIDs.forEach { ssid -> -// ClickableIconButton( -// onClick = { -// if (isRunningOnTv) { -// focusRequester.requestFocus() -// viewModel.onDeleteTrustedSSID(ssid) -// } -// }, -// onIconClick = { -// if (isRunningOnTv) focusRequester.requestFocus() -// viewModel.onDeleteTrustedSSID(ssid) -// }, -// text = ssid, -// icon = Icons.Filled.Close, -// ) -// } -// if (uiState.settings.trustedNetworkSSIDs.isEmpty()) { -// Text( -// stringResource(R.string.none), -// fontStyle = FontStyle.Italic, -// style = MaterialTheme.typography.bodySmall, -// color = MaterialTheme.colorScheme.onSurface, -// ) -// } -// } -// OutlinedTextField( -// value = currentText, -// onValueChange = { currentText = it }, -// label = { Text(stringResource(R.string.add_trusted_ssid)) }, -// modifier = -// Modifier -// .padding( -// start = screenPadding, -// top = 5.dp, -// bottom = 10.dp, -// ), -// supportingText = { WildcardSupportingLabel { context.openWebUrl(it) } }, -// maxLines = 1, -// keyboardOptions = -// KeyboardOptions( -// capitalization = KeyboardCapitalization.None, -// imeAction = ImeAction.Done, -// ), -// keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }), -// trailingIcon = { -// if (currentText != "") { -// IconButton(onClick = { saveTrustedSSID() }) { -// Icon( -// imageVector = Icons.Outlined.Add, -// contentDescription = -// if (currentText == "") { -// stringResource( -// id = -// R.string -// .trusted_ssid_empty_description, -// ) -// } else { -// stringResource( -// id = -// R.string -// .trusted_ssid_value_description, -// ) -// }, -// tint = MaterialTheme.colorScheme.primary, -// ) -// } -// } -// }, -// ) -// } -// } -// ConfigurationToggle( -// stringResource(R.string.tunnel_mobile_data), -// enabled = !uiState.settings.isAlwaysOnVpnEnabled, -// checked = uiState.settings.isTunnelOnMobileDataEnabled, -// onCheckChanged = { viewModel.onToggleTunnelOnMobileData() }, -// ) -// ConfigurationToggle( -// stringResource(id = R.string.tunnel_on_ethernet), -// enabled = !uiState.settings.isAlwaysOnVpnEnabled, -// checked = uiState.settings.isTunnelOnEthernetEnabled, -// onCheckChanged = { viewModel.onToggleTunnelOnEthernet() }, -// ) -// ConfigurationToggle( -// stringResource(R.string.restart_on_ping), -// checked = uiState.settings.isPingEnabled, -// onCheckChanged = { viewModel.onToggleRestartOnPing() }, -// ) -// Row( -// verticalAlignment = Alignment.CenterVertically, -// modifier = -// ( -// if (!uiState.settings.isAutoTunnelEnabled) { -// Modifier -// } else { -// Modifier.focusRequester( -// focusRequester, -// ) -// } -// ) -// .fillMaxSize() -// .padding(top = 5.dp), -// horizontalArrangement = Arrangement.Center, -// ) { -// TextButton( -// onClick = { -// if (uiState.tunnels.isEmpty()) return@TextButton context.showToast(R.string.tunnel_required) -// handleAutoTunnelToggle() -// }, -// ) { -// val autoTunnelButtonText = -// if (uiState.settings.isAutoTunnelEnabled) { -// stringResource(R.string.disable_auto_tunnel) -// } else { -// stringResource(id = R.string.enable_auto_tunnel) -// } -// Text(autoTunnelButtonText) -// } -// } -// } -// } -// Surface( -// tonalElevation = 2.dp, -// shadowElevation = 2.dp, -// shape = RoundedCornerShape(12.dp), -// color = MaterialTheme.colorScheme.surface, -// modifier = -// Modifier -// .fillMaxWidth(fillMaxWidth) -// .padding(vertical = 10.dp), -// ) { -// Column( -// horizontalAlignment = Alignment.Start, -// verticalArrangement = Arrangement.Top, -// modifier = Modifier.padding(15.dp), -// ) { -// SectionTitle( -// title = stringResource(id = R.string.backend), -// padding = screenPadding, -// ) -// ConfigurationToggle( -// stringResource(R.string.use_kernel), -// enabled = -// !( -// uiState.settings.isAutoTunnelEnabled || -// uiState.settings.isAlwaysOnVpnEnabled || -// (uiState.vpnState.status == TunnelState.UP) || -// !settingsUiState.isKernelAvailable -// ), -// checked = uiState.settings.isKernelEnabled, -// onCheckChanged = { -// viewModel.onToggleKernelMode() -// }, -// ) -// Row( -// verticalAlignment = Alignment.CenterVertically, -// modifier = -// Modifier -// .fillMaxSize() -// .padding(top = 5.dp), -// horizontalArrangement = Arrangement.Center, -// ) { -// TextButton( -// onClick = { -// viewModel.onRequestRoot() -// }, -// ) { -// Text(stringResource(R.string.request_root)) -// } -// } -// } -// } -// Surface( -// tonalElevation = 2.dp, -// shadowElevation = 2.dp, -// shape = RoundedCornerShape(12.dp), -// color = MaterialTheme.colorScheme.surface, -// modifier = -// Modifier -// .fillMaxWidth(fillMaxWidth) -// .padding(vertical = 10.dp) -// .padding(bottom = 10.dp), -// ) { -// Column( -// horizontalAlignment = Alignment.Start, -// verticalArrangement = Arrangement.Top, -// modifier = Modifier.padding(15.dp), -// ) { -// SectionTitle( -// title = stringResource(id = R.string.other), -// padding = screenPadding, -// ) -// if (!isRunningOnTv) { -// ConfigurationToggle( -// stringResource(R.string.always_on_vpn_support), -// enabled = !( -// ( -// uiState.settings.isTunnelOnWifiEnabled || -// uiState.settings.isTunnelOnEthernetEnabled || -// uiState.settings.isTunnelOnMobileDataEnabled -// ) && -// uiState.settings.isAutoTunnelEnabled -// ), -// checked = uiState.settings.isAlwaysOnVpnEnabled, -// onCheckChanged = { viewModel.onToggleAlwaysOnVPN() }, -// ) -// ConfigurationToggle( -// stringResource(R.string.enabled_app_shortcuts), -// enabled = true, -// checked = uiState.settings.isShortcutsEnabled, -// onCheckChanged = { viewModel.onToggleShortcutsEnabled() }, -// ) -// } -// ConfigurationToggle( -// stringResource(R.string.restart_at_boot), -// enabled = true, -// checked = uiState.settings.isRestoreOnBootEnabled, -// onCheckChanged = { -// viewModel.onToggleRestartAtBoot() -// }, -// ) -// ConfigurationToggle( -// stringResource(R.string.enable_app_lock), -// enabled = true, -// checked = uiState.generalState.isPinLockEnabled, -// onCheckChanged = { -// if (uiState.generalState.isPinLockEnabled) { -// appViewModel.onPinLockDisabled() -// } else { -// // TODO may want to show a dialog before proceeding in the future -// PinManager.initialize(WireGuardAutoTunnel.instance) -// navController.navigate(Route.Lock) -// } -// }, -// ) -// if (!isRunningOnTv) { -// Row( -// verticalAlignment = Alignment.CenterVertically, -// modifier = -// Modifier -// .fillMaxSize() -// .padding(top = 5.dp), -// horizontalArrangement = Arrangement.Center, -// ) { -// TextButton( -// enabled = !didExportFiles, -// onClick = { -// if (uiState.tunnels.isEmpty()) return@TextButton context.showToast(R.string.tunnel_required) -// showAuthPrompt = true -// }, -// ) { -// Text(stringResource(R.string.export_configs)) -// } -// } -// } -// } -// } + if (!isRunningOnTv) { + SurfaceSelectionGroupButton( + listOf( + SelectionItem( + Icons.Outlined.FolderZip, + title = { + Text( + stringResource(R.string.export_configs), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + onClick = { + if (uiState.tunnels.isEmpty()) return@SelectionItem context.showToast(R.string.tunnel_required) + showAuthPrompt = true + }, + ), + ), + ) + } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsUiState.kt deleted file mode 100644 index f2a3a68..0000000 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsUiState.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.zaneschepke.wireguardautotunnel.ui.screens.settings - -data class SettingsUiState( - val isRooted: Boolean = false, - val isKernelAvailable: Boolean = false, -) 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 a5f25b6..d3ce4e9 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 @@ -10,19 +10,14 @@ import com.zaneschepke.wireguardautotunnel.R 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.ui.common.snackbar.SnackbarController -import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.FileUtils import com.zaneschepke.wireguardautotunnel.util.StringValue import com.zaneschepke.wireguardautotunnel.util.extensions.launchShareFile import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.time.Instant @@ -39,16 +34,6 @@ constructor( @IoDispatcher private val ioDispatcher: CoroutineDispatcher, ) : ViewModel() { - private val _uiState = MutableStateFlow(SettingsUiState()) - val uiState = _uiState.onStart { - _uiState.update { - it.copy(isKernelAvailable = isKernelSupported(), isRooted = isRooted()) - } - }.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), - SettingsUiState(), - ) private val settings = appDataRepository.settings.getSettingsFlow() .stateIn(viewModelScope, SharingStarted.Eagerly, Settings()) @@ -56,10 +41,6 @@ constructor( appDataRepository.appState.setLocationDisclosureShown(true) } - fun setBatteryOptimizeDisableShown() = viewModelScope.launch { - appDataRepository.appState.setBatteryOptimizationDisableShown(true) - } - fun onToggleAlwaysOnVPN() = viewModelScope.launch { with(settings.value) { appDataRepository.settings.save( @@ -90,23 +71,11 @@ constructor( } } - fun onToggleAmnezia() = viewModelScope.launch { - with(settings.value) { - if (isKernelEnabled) { - saveKernelMode(false) - } - appDataRepository.settings.save( - copy( - isAmneziaEnabled = !isAmneziaEnabled, - ), - ) - } - } - fun onToggleKernelMode() = viewModelScope.launch { with(settings.value) { if (!isKernelEnabled) { requestRoot().onSuccess { + if (!isKernelSupported()) return@onSuccess SnackbarController.showMessage(StringValue.StringResource(R.string.kernel_not_supported)) appDataRepository.settings.save( copy( isKernelEnabled = true, @@ -136,17 +105,6 @@ constructor( } } - private suspend fun isRooted(): Boolean { - return try { - withContext(ioDispatcher) { - rootShell.get().start() - } - true - } catch (_: Exception) { - false - } - } - private suspend fun requestRoot(): Result { return withContext(ioDispatcher) { kotlin.runCatching { @@ -158,10 +116,6 @@ constructor( } } - fun onRequestRoot() = viewModelScope.launch { - requestRoot() - } - fun exportAllConfigs(context: Context) = viewModelScope.launch { kotlin.runCatching { val shareFile = fileUtils.createNewShareFile("wg-export_${Instant.now().epochSecond}.zip") diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/AppearanceScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/AppearanceScreen.kt index e25db8f..3f947d3 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/AppearanceScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/AppearanceScreen.kt @@ -32,8 +32,8 @@ fun AppearanceScreen() { Scaffold( topBar = { TopNavBar(stringResource(R.string.appearance)) - } - ){ + }, + ) { Column( horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top), @@ -51,7 +51,7 @@ fun AppearanceScreen() { onClick = { navController.navigate(Route.Language) }, trailing = { ForwardButton { navController.navigate(Route.Language) } - } + }, ), ), ) @@ -63,7 +63,7 @@ fun AppearanceScreen() { onClick = { navController.navigate(Route.Display) }, trailing = { ForwardButton { navController.navigate(Route.Display) } - } + }, ), ), ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/display/DisplayScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/display/DisplayScreen.kt index d6f9c8a..f6fc0e4 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/display/DisplayScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/display/DisplayScreen.kt @@ -2,11 +2,8 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.displ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -24,11 +21,10 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth @Composable fun DisplayScreen(appUiState: AppUiState, viewModel: DisplayViewModel = hiltViewModel()) { - Scaffold( topBar = { TopNavBar(stringResource(R.string.display_theme)) - } + }, ) { Column( horizontalAlignment = Alignment.Start, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/display/DisplayViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/display/DisplayViewModel.kt index c8714c1..cbcaaec 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/display/DisplayViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/display/DisplayViewModel.kt @@ -12,7 +12,7 @@ import javax.inject.Inject class DisplayViewModel @Inject constructor( - private val appStateRepository: AppStateRepository + private val appStateRepository: AppStateRepository, ) : ViewModel() { fun onThemeChange(theme: Theme) = viewModelScope.launch { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/language/LanguageScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/language/LanguageScreen.kt index 7015783..aa7df31 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/language/LanguageScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/language/LanguageScreen.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -69,7 +68,7 @@ fun LanguageScreen(localeStorage: LocaleStorage) { Scaffold( topBar = { TopNavBar(stringResource(R.string.language)) - } + }, ) { LazyColumn( horizontalAlignment = Alignment.CenterHorizontally, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelScreen.kt index ad360c3..a6094c6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelScreen.kt @@ -1,6 +1,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel import android.Manifest +import android.os.Build import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi @@ -11,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Code import androidx.compose.material.icons.outlined.Filter1 import androidx.compose.material.icons.outlined.NetworkPing import androidx.compose.material.icons.outlined.Security @@ -31,7 +33,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.google.accompanist.permissions.ExperimentalPermissionsApi @@ -45,9 +46,12 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelec import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.TrustedNetworkTextBox import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.WildcardsLabel +import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackgroundLocationDialog import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LearnMoreLinkLabel +import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LocationServicesDialog import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize import com.zaneschepke.wireguardautotunnel.util.extensions.isLocationServicesEnabled +import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth @@ -63,11 +67,12 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV var showLocationServicesAlertDialog by remember { mutableStateOf(false) } var showLocationDialog by remember { mutableStateOf(false) } - LaunchedEffect(uiState.settings.trustedNetworkSSIDs) { - currentText = "" + fun checkFineLocationGranted() { + isBackgroundLocationGranted = fineLocationState.status.isGranted } fun onAutoTunnelWifiChecked() { + if (uiState.settings.isTunnelOnWifiEnabled) viewModel.onToggleTunnelOnWifi().also { return } when (false) { isBackgroundLocationGranted -> showLocationDialog = true fineLocationState.status.isGranted -> showLocationDialog = true @@ -79,11 +84,39 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV } } + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) checkFineLocationGranted() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (context.isRunningOnTv() && Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { + checkFineLocationGranted() + } else { + val backgroundLocationState = rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION) + isBackgroundLocationGranted = backgroundLocationState.status.isGranted + } + } + + LaunchedEffect(uiState.settings.trustedNetworkSSIDs) { + currentText = "" + } + + LocationServicesDialog( + showLocationServicesAlertDialog, + onDismiss = { showLocationServicesAlertDialog = false }, + onAttest = { + viewModel.onToggleTunnelOnWifi() + }, + ) + + BackgroundLocationDialog( + showLocationDialog, + onDismiss = { showLocationDialog = false }, + onAttest = { showLocationDialog = false }, + ) + Scaffold( contentWindowInsets = WindowInsets(0.dp), topBar = { TopNavBar(stringResource(R.string.auto_tunneling)) - } + }, ) { Column( horizontalAlignment = Alignment.Start, @@ -97,34 +130,60 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV ) { SurfaceSelectionGroupButton( buildList { - add( - SelectionItem( - Icons.Outlined.Wifi, - title = { - Text( - stringResource(R.string.tunnel_on_wifi), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface) - ) - }, - description = { - }, - trailing = { - ScaledSwitch( - enabled = !uiState.settings.isAlwaysOnVpnEnabled, - checked = uiState.settings.isTunnelOnWifiEnabled, - onClick = { - if (!uiState.settings.isTunnelOnWifiEnabled || uiState.isRooted) viewModel.onToggleTunnelOnWifi() - .also { return@ScaledSwitch } - onAutoTunnelWifiChecked() - }, - ) - }, - onClick = { - if (!uiState.settings.isTunnelOnWifiEnabled || uiState.isRooted) viewModel.onToggleTunnelOnWifi() - .also { return@SelectionItem } - onAutoTunnelWifiChecked() - } - ) + addAll( + listOf( + SelectionItem( + Icons.Outlined.Wifi, + title = { + Text( + stringResource(R.string.tunnel_on_wifi), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + description = { + }, + trailing = { + ScaledSwitch( + enabled = !uiState.settings.isAlwaysOnVpnEnabled, + checked = uiState.settings.isTunnelOnWifiEnabled, + onClick = { + if (uiState.settings.isWifiNameByShellEnabled) viewModel.onToggleTunnelOnWifi().also { return@ScaledSwitch } + onAutoTunnelWifiChecked() + }, + ) + }, + onClick = { + if (uiState.settings.isWifiNameByShellEnabled) viewModel.onToggleTunnelOnWifi().also { return@SelectionItem } + onAutoTunnelWifiChecked() + }, + ), + SelectionItem( + Icons.Outlined.Code, + title = { + Text( + stringResource(R.string.wifi_name_via_shell), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + description = { + Text( + stringResource(R.string.use_root_shell_for_wifi), + style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline), + ) + }, + trailing = { + ScaledSwitch( + checked = uiState.settings.isWifiNameByShellEnabled, + onClick = { + viewModel.onRootShellWifiToggle() + }, + ) + }, + onClick = { + viewModel.onRootShellWifiToggle() + }, + ), + ), ) if (uiState.settings.isTunnelOnWifiEnabled) { addAll( @@ -134,15 +193,15 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV title = { Text( stringResource(R.string.use_wildcards), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface) + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), ) }, description = { - LearnMoreLinkLabel({context.openWebUrl(it)}, stringResource(id = R.string.docs_wildcards)) + LearnMoreLinkLabel({ context.openWebUrl(it) }, stringResource(id = R.string.docs_wildcards)) }, trailing = { ScaledSwitch( - checked = uiState.generalState.isWildcardsEnabled, + checked = uiState.settings.isWildcardsEnabled, onClick = { viewModel.onToggleWildcards() }, @@ -150,7 +209,7 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV }, onClick = { viewModel.onToggleWildcards() - } + }, ), SelectionItem( title = { @@ -183,87 +242,89 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), ) } - } } - }, description = { TrustedNetworkTextBox( - uiState.settings.trustedNetworkSSIDs, onDelete = viewModel::onDeleteTrustedSSID, + uiState.settings.trustedNetworkSSIDs, + onDelete = viewModel::onDeleteTrustedSSID, currentText = currentText, onSave = viewModel::onSaveTrustedSSID, onValueChange = { currentText = it }, - supporting = { if(uiState.generalState.isWildcardsEnabled) { - WildcardsLabel() - }} + supporting = { + if (uiState.settings.isWildcardsEnabled) { + WildcardsLabel() + } + }, ) }, - ) - )) + ), + ), + ) } - } + }, ) SurfaceSelectionGroupButton( - listOf( - SelectionItem( - Icons.Outlined.SignalCellular4Bar, - title = { - Text( - stringResource(R.string.tunnel_mobile_data), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - trailing = { - ScaledSwitch( - enabled = !uiState.settings.isAlwaysOnVpnEnabled, - checked = uiState.settings.isTunnelOnMobileDataEnabled, - onClick = { viewModel.onToggleTunnelOnMobileData() }, - ) - }, - onClick = { - viewModel.onToggleTunnelOnMobileData() - } - ), - SelectionItem( - Icons.Outlined.SettingsEthernet, - title = { - Text( - stringResource(R.string.tunnel_on_ethernet), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - trailing = { - ScaledSwitch( - enabled = !uiState.settings.isAlwaysOnVpnEnabled, - checked = uiState.settings.isTunnelOnEthernetEnabled, - onClick = { viewModel.onToggleTunnelOnEthernet() }, - ) - }, - onClick = { - viewModel.onToggleTunnelOnEthernet() - } - ), - SelectionItem( - Icons.Outlined.NetworkPing, - title = { - Text( - stringResource(R.string.restart_on_ping), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - trailing = { - ScaledSwitch( - checked = uiState.settings.isPingEnabled, - onClick = { viewModel.onToggleRestartOnPing() }, - ) - }, - onClick = { - viewModel.onToggleRestartOnPing() - } + listOf( + SelectionItem( + Icons.Outlined.SignalCellular4Bar, + title = { + Text( + stringResource(R.string.tunnel_mobile_data), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), ) - ) - ) + }, + trailing = { + ScaledSwitch( + enabled = !uiState.settings.isAlwaysOnVpnEnabled, + checked = uiState.settings.isTunnelOnMobileDataEnabled, + onClick = { viewModel.onToggleTunnelOnMobileData() }, + ) + }, + onClick = { + viewModel.onToggleTunnelOnMobileData() + }, + ), + SelectionItem( + Icons.Outlined.SettingsEthernet, + title = { + Text( + stringResource(R.string.tunnel_on_ethernet), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + trailing = { + ScaledSwitch( + enabled = !uiState.settings.isAlwaysOnVpnEnabled, + checked = uiState.settings.isTunnelOnEthernetEnabled, + onClick = { viewModel.onToggleTunnelOnEthernet() }, + ) + }, + onClick = { + viewModel.onToggleTunnelOnEthernet() + }, + ), + SelectionItem( + Icons.Outlined.NetworkPing, + title = { + Text( + stringResource(R.string.restart_on_ping), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + trailing = { + ScaledSwitch( + checked = uiState.settings.isPingEnabled, + onClick = { viewModel.onToggleRestartOnPing() }, + ) + }, + onClick = { + viewModel.onToggleRestartOnPing() + }, + ), + ), + ) } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelViewModel.kt index 1449c41..55c787c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelViewModel.kt @@ -2,22 +2,29 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.wireguard.android.util.RootShell import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository +import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController import com.zaneschepke.wireguardautotunnel.util.StringValue import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject +import javax.inject.Provider @HiltViewModel class AutoTunnelViewModel @Inject constructor( private val appDataRepository: AppDataRepository, + private val rootShell: Provider, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, ) : ViewModel() { private val settings = appDataRepository.settings.getSettingsFlow() @@ -37,29 +44,53 @@ constructor( with(settings.value) { appDataRepository.settings.save( copy( - isTunnelOnMobileDataEnabled = !this.isTunnelOnMobileDataEnabled, + isTunnelOnMobileDataEnabled = !isTunnelOnMobileDataEnabled, ), ) } } fun onToggleWildcards() = viewModelScope.launch { - val wildcards = appDataRepository.appState.isWildcardsEnabled() - appDataRepository.appState.setWildcardsEnabled( - !wildcards - ) + with(settings.value) { + appDataRepository.settings.save( + copy( + isWildcardsEnabled = !isWildcardsEnabled, + ), + ) + } } fun onDeleteTrustedSSID(ssid: String) = viewModelScope.launch { with(settings.value) { appDataRepository.settings.save( copy( - trustedNetworkSSIDs = (this.trustedNetworkSSIDs - ssid).toMutableList(), + trustedNetworkSSIDs = (trustedNetworkSSIDs - ssid).toMutableList(), ), ) } } + fun onRootShellWifiToggle() = viewModelScope.launch { + requestRoot().onSuccess { + with(settings.value) { + appDataRepository.settings.save( + copy(isWifiNameByShellEnabled = !isWifiNameByShellEnabled), + ) + } + } + } + + private suspend fun requestRoot(): Result { + return withContext(ioDispatcher) { + kotlin.runCatching { + rootShell.get().start() + SnackbarController.showMessage(StringValue.StringResource(R.string.root_accepted)) + }.onFailure { + SnackbarController.showMessage(StringValue.StringResource(R.string.error_root_denied)) + } + } + } + fun onToggleTunnelOnEthernet() = viewModelScope.launch { with(settings.value) { appDataRepository.settings.save( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/components/TrustNetworksTextBox.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/components/TrustNetworksTextBox.kt index 635be17..30a7430 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/components/TrustNetworksTextBox.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/components/TrustNetworksTextBox.kt @@ -32,9 +32,16 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth @OptIn(ExperimentalLayoutApi::class) @Composable -fun TrustedNetworkTextBox(trustedNetworks: List, onDelete: (ssid: String) -> Unit, currentText: String, onSave : (ssid: String) -> Unit, onValueChange: (network: String) -> Unit, supporting: @Composable () -> Unit) { +fun TrustedNetworkTextBox( + trustedNetworks: List, + onDelete: (ssid: String) -> Unit, + currentText: String, + onSave: (ssid: String) -> Unit, + onValueChange: (network: String) -> Unit, + supporting: @Composable () -> Unit, +) { val context = LocalContext.current - Column(verticalArrangement = Arrangement.spacedBy(10.dp.scaledHeight())){ + Column(verticalArrangement = Arrangement.spacedBy(10.dp.scaledHeight())) { FlowRow( modifier = Modifier.fillMaxWidth(), @@ -93,6 +100,5 @@ fun TrustedNetworkTextBox(trustedNetworks: List, onDelete: (ssid: String } }, ) - } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/components/WildcardsLabel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/components/WildcardsLabel.kt index 56039be..d52a9d9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/components/WildcardsLabel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/components/WildcardsLabel.kt @@ -7,7 +7,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontStyle import com.zaneschepke.wireguardautotunnel.R - @Composable fun WildcardsLabel() { Text( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/ForwardButton.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/ForwardButton.kt index 675b5d3..f217a9e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/ForwardButton.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/ForwardButton.kt @@ -14,7 +14,7 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize fun ForwardButton(modifier: Modifier = Modifier.focusable(), onClick: () -> Unit) { IconButton( modifier = modifier, - onClick = onClick + onClick = onClick, ) { val icon = Icons.AutoMirrored.Outlined.ArrowForward Icon(icon, icon.name, Modifier.size(iconSize)) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/LearnMoreLinkLabel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/LearnMoreLinkLabel.kt index 453d368..4724e8d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/LearnMoreLinkLabel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/LearnMoreLinkLabel.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.text.withStyle import com.zaneschepke.wireguardautotunnel.R @Composable -fun LearnMoreLinkLabel(onClick: (url: String) -> Unit, url : String) { +fun LearnMoreLinkLabel(onClick: (url: String) -> Unit, url: String) { // TODO update link when docs are fully updated val gettingStarted = buildAnnotatedString { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/disclosure/LocationDisclosureScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/disclosure/LocationDisclosureScreen.kt index 63fd324..e21ac68 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/disclosure/LocationDisclosureScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/disclosure/LocationDisclosureScreen.kt @@ -39,7 +39,7 @@ fun LocationDisclosureScreen(appViewModel: AppViewModel, appUiState: AppUiState) val navController = LocalNavController.current LaunchedEffect(Unit, appUiState) { - if(appUiState.generalState.isLocationDisclosureShown) navController.goFromRoot(Route.AutoTunnel) + if (appUiState.generalState.isLocationDisclosureShown) navController.goFromRoot(Route.AutoTunnel) } Column( @@ -56,40 +56,49 @@ fun LocationDisclosureScreen(appViewModel: AppViewModel, appUiState: AppUiState) .padding(30.dp.scaledHeight()) .size(128.dp.scaledHeight()), ) - Text( - stringResource(R.string.prominent_background_location_title), - style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold), - ) - Text( - stringResource(R.string.prominent_background_location_message), - style = MaterialTheme.typography.bodyLarge - ) - SurfaceSelectionGroupButton( - listOf( - SelectionItem( - Icons.Outlined.LocationOn, - title = { Text(stringResource(R.string.launch_app_settings), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, - onClick = { context.launchAppSettings().also { + Text( + stringResource(R.string.prominent_background_location_title), + style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold), + ) + Text( + stringResource(R.string.prominent_background_location_message), + style = MaterialTheme.typography.bodyLarge, + ) + SurfaceSelectionGroupButton( + listOf( + SelectionItem( + Icons.Outlined.LocationOn, + title = { + Text( + stringResource(R.string.launch_app_settings), + style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + onClick = { + context.launchAppSettings().also { appViewModel.setLocationDisclosureShown() - } }, - trailing = { - ForwardButton { context.launchAppSettings().also { + } + }, + trailing = { + ForwardButton { + context.launchAppSettings().also { appViewModel.setLocationDisclosureShown() - } } + } } - ), + }, ), - ) - SurfaceSelectionGroupButton( - listOf( - SelectionItem( - title = { Text(stringResource(R.string.skip), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, - onClick = { appViewModel.setLocationDisclosureShown() }, - trailing = { - ForwardButton { appViewModel.setLocationDisclosureShown() } - } - ), + ), + ) + SurfaceSelectionGroupButton( + listOf( + SelectionItem( + title = { Text(stringResource(R.string.skip), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, + onClick = { appViewModel.setLocationDisclosureShown() }, + trailing = { + ForwardButton { appViewModel.setLocationDisclosureShown() } + }, ), - ) - } + ), + ) } +} 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 561d637..30c8bd5 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 @@ -38,94 +38,118 @@ fun SupportScreen() { val context = LocalContext.current val navController = LocalNavController.current - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top), - modifier = - Modifier - .fillMaxSize() - .padding(top = topPadding) - .padding(horizontal = 24.dp.scaledWidth()), - ) { - GroupLabel(stringResource(R.string.thank_you)) - SurfaceSelectionGroupButton( - listOf( - SelectionItem( - Icons.Filled.Book, - title = { Text(stringResource(R.string.docs_description), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, - trailing = { - ForwardButton { context.openWebUrl(context.getString(R.string.docs_url)) } - }, - onClick = { - context.openWebUrl(context.getString(R.string.docs_url)) - } - ), - SelectionItem( - Icons.Filled.LineStyle, - title = { Text(stringResource(R.string.read_logs), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, - trailing = { - ForwardButton { - navController.navigate(Route.Logs) - } - }, - onClick = { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top), + modifier = + Modifier + .fillMaxSize() + .padding(top = topPadding) + .padding(horizontal = 24.dp.scaledWidth()), + ) { + GroupLabel(stringResource(R.string.thank_you)) + SurfaceSelectionGroupButton( + listOf( + SelectionItem( + Icons.Filled.Book, + title = { + Text( + stringResource(R.string.docs_description), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + trailing = { + ForwardButton { context.openWebUrl(context.getString(R.string.docs_url)) } + }, + onClick = { + context.openWebUrl(context.getString(R.string.docs_url)) + }, + ), + SelectionItem( + Icons.Filled.LineStyle, + title = { + Text( + stringResource(R.string.read_logs), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + trailing = { + ForwardButton { navController.navigate(Route.Logs) } - ), - SelectionItem( - Icons.Filled.Policy, - title = { Text(stringResource(R.string.privacy_policy), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, - trailing = { - ForwardButton { context.openWebUrl(context.getString(R.string.privacy_policy_url)) } - }, - onClick = { - context.openWebUrl(context.getString(R.string.privacy_policy_url)) - } - ), + }, + onClick = { + navController.navigate(Route.Logs) + }, + ), + SelectionItem( + Icons.Filled.Policy, + title = { + Text( + stringResource(R.string.privacy_policy), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + trailing = { + ForwardButton { context.openWebUrl(context.getString(R.string.privacy_policy_url)) } + }, + onClick = { + context.openWebUrl(context.getString(R.string.privacy_policy_url)) + }, + ), - ) - ) - SurfaceSelectionGroupButton( - listOf( - SelectionItem( - ImageVector.vectorResource(R.drawable.telegram), - title = { Text(stringResource(R.string.chat_description), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, - trailing = { - ForwardButton { - context.openWebUrl(context.getString(R.string.telegram_url)) - } - }, - onClick = { + ), + ) + SurfaceSelectionGroupButton( + listOf( + SelectionItem( + ImageVector.vectorResource(R.drawable.telegram), + title = { + Text( + stringResource(R.string.chat_description), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + trailing = { + ForwardButton { context.openWebUrl(context.getString(R.string.telegram_url)) } - ), - SelectionItem( - ImageVector.vectorResource(R.drawable.github), - title = { Text(stringResource(R.string.open_issue), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, - trailing = { - ForwardButton { - context.openWebUrl(context.getString(R.string.github_url)) - } - }, - onClick = { + }, + onClick = { + context.openWebUrl(context.getString(R.string.telegram_url)) + }, + ), + SelectionItem( + ImageVector.vectorResource(R.drawable.github), + title = { Text(stringResource(R.string.open_issue), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, + trailing = { + ForwardButton { context.openWebUrl(context.getString(R.string.github_url)) } - ), - SelectionItem( - Icons.Filled.Mail, - title = { Text(stringResource(R.string.email_description), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, - trailing = { - ForwardButton { - context.launchSupportEmail() - } - }, - onClick = { + }, + onClick = { + context.openWebUrl(context.getString(R.string.github_url)) + }, + ), + SelectionItem( + Icons.Filled.Mail, + title = { + Text( + stringResource(R.string.email_description), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + trailing = { + ForwardButton { context.launchSupportEmail() } - ), - ) - ) - VersionLabel() - } + }, + onClick = { + context.launchSupportEmail() + }, + ), + ), + ) + VersionLabel() } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsScreen.kt index 96c7f40..918826a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsScreen.kt @@ -4,11 +4,8 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Color.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Color.kt index 1caf5e2..5adc93a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Color.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Color.kt @@ -12,8 +12,6 @@ val BalticSea = Color(0xFF1C1B1F) val Brick = Color(0xFFCE4257) val Straw = Color(0xFFD4C483) - - sealed class ThemeColors( val background: Color, val surface: Color, 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 bb4eeef..e1e30cc 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 @@ -40,18 +40,15 @@ enum class Theme { AUTOMATIC, LIGHT, DARK, - DYNAMIC + DYNAMIC, } @Composable -fun WireguardAutoTunnelTheme( - theme: Theme = Theme.AUTOMATIC, - content: @Composable () -> Unit, -) { +fun WireguardAutoTunnelTheme(theme: Theme = Theme.AUTOMATIC, content: @Composable () -> Unit) { val context = LocalContext.current var isDark = isSystemInDarkTheme() - val autoTheme = if(isDark) DarkColorScheme else LightColorScheme - val colorScheme = when(theme) { + val autoTheme = if (isDark) DarkColorScheme else LightColorScheme + val colorScheme = when (theme) { Theme.AUTOMATIC -> autoTheme Theme.DARK -> { isDark = true @@ -68,7 +65,9 @@ fun WireguardAutoTunnelTheme( } else { dynamicLightColorScheme(context) } - } else autoTheme + } else { + autoTheme + } } } val view = LocalView.current diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/SingletonHolder.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/SingletonHolder.kt new file mode 100644 index 0000000..6d695f6 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/SingletonHolder.kt @@ -0,0 +1,26 @@ +package com.zaneschepke.wireguardautotunnel.util + +open class SingletonHolder(creator: (A) -> T) { + private var creator: ((A) -> T)? = creator + + @Volatile private var instance: T? = null + + fun getInstance(arg: A): T { + val i = instance + if (i != null) { + return i + } + + return synchronized(this) { + val i2 = instance + if (i2 != null) { + i2 + } else { + val created = creator!!(arg) + instance = created + creator = null + created + } + } + } +} 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 index 18a3d2f..42377e9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/ContextExtensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/ContextExtensions.kt @@ -2,10 +2,12 @@ package com.zaneschepke.wireguardautotunnel.util.extensions import android.content.ComponentName import android.content.Context +import android.content.Context.POWER_SERVICE import android.content.Intent import android.content.pm.PackageManager import android.location.LocationManager import android.net.Uri +import android.os.PowerManager import android.provider.Settings import android.service.quicksettings.TileService import android.widget.Toast @@ -13,7 +15,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.TextUnit import androidx.core.location.LocationManagerCompat 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 @@ -34,6 +35,11 @@ fun Context.openWebUrl(url: String): Result { } } +fun Context.isBatteryOptimizationsDisabled(): Boolean { + val pm = getSystemService(POWER_SERVICE) as PowerManager + return pm.isIgnoringBatteryOptimizations(packageName) +} + val Context.actionBarSize get() = theme.obtainStyledAttributes(intArrayOf(android.R.attr.actionBarSize)) .let { attrs -> attrs.getDimension(0, 0F).toInt().also { attrs.recycle() } } @@ -66,7 +72,7 @@ fun Context.resizeWidth(dp: Dp): Dp { } fun Context.launchNotificationSettings() { - if(isRunningOnTv()) return launchAppSettings() + if (isRunningOnTv()) return launchAppSettings() val settingsIntent: Intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .putExtra(Settings.EXTRA_APP_PACKAGE, packageName) @@ -159,23 +165,23 @@ fun Context.launchAppSettings() { } } -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.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( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/StringExtensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/StringExtensions.kt index 7a617c5..48ae90a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/StringExtensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/StringExtensions.kt @@ -28,9 +28,9 @@ fun String.extractNameAndNumber(): Pair? { } fun List.isMatchingToWildcardList(value: String): Boolean { - val excludeValues = this.filter { it.startsWith("!") }.map { it.removePrefix("!").toRegexWithWildcards() } + val excludeValues = this.filter { it.startsWith("!") }.map { it.removePrefix("!").transformWildcardsToRegex() } Timber.d("Excluded values: $excludeValues") - val includedValues = this.filter { !it.startsWith("!") }.map { it.toRegexWithWildcards() } + val includedValues = this.filter { !it.startsWith("!") }.map { it.transformWildcardsToRegex() } Timber.d("Included values: $includedValues") val matches = includedValues.filter { it.matches(value) } val excludedMatches = excludeValues.filter { it.matches(value) } @@ -39,6 +39,32 @@ fun List.isMatchingToWildcardList(value: String): Boolean { return matches.isNotEmpty() && excludedMatches.isEmpty() } -fun String.toRegexWithWildcards(): Regex { - return this.replace("*", ".*").replace("?", ".").toRegex() +fun String.transformWildcardsToRegex(): Regex { + return this.replaceUnescapedChar("*", ".*").replaceUnescapedChar("?", ".").toRegex() +} + +fun String.replaceUnescapedChar(charToReplace: String, replacement: String): String { + val escapedChar = Regex.escape(charToReplace) + val regex = "(? + if (matchResult.range.first == 0 || + this[matchResult.range.first - 1] != '\\' || + (matchResult.range.first > 1 && this[matchResult.range.first - 2] == '\\') + ) { + replacement.toString() + } else { + matchResult.value + } + } +} + +fun String.isCharacterEscaped(index: Int): Boolean { + if (index <= 0) return false + var backslashCount = 0 + var currentIndex = index - 1 + while (currentIndex >= 0 && this[currentIndex] == '\\') { + backslashCount++ + currentIndex-- + } + return backslashCount % 2 != 0 } 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 index c39f15c..34405a9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt @@ -4,9 +4,11 @@ import androidx.compose.ui.graphics.Color import com.wireguard.android.util.RootShell import com.wireguard.config.Peer import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus +import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState +import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics -import com.zaneschepke.wireguardautotunnel.ui.theme.Straw import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree +import com.zaneschepke.wireguardautotunnel.ui.theme.Straw import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.NumberUtils import org.amnezia.awg.config.Config @@ -21,6 +23,10 @@ fun TunnelStatistics.PeerStats.latestHandshakeSeconds(): Long? { return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis) } +fun VpnState.isDown(): Boolean { + return this.status == TunnelState.DOWN +} + fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus { // TODO add never connected status after duration return this.latestHandshakeSeconds().let { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 48ebb9d..1777125 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -220,4 +220,9 @@ Use name wildcards Learn more Wildcards active + Wifi name via shell + Use root shell to get wifi name + Kernel not supported + Start auto-tunnel + Stop auto-tunnel diff --git a/app/src/main/res/xml/shortcuts.xml b/app/src/main/res/xml/shortcuts.xml index c4f61c1..bfd59e9 100644 --- a/app/src/main/res/xml/shortcuts.xml +++ b/app/src/main/res/xml/shortcuts.xml @@ -37,8 +37,8 @@ android:enabled="true" android:icon="@drawable/auto_play" android:shortcutId="autoOn1" - android:shortcutLongLabel="@string/auto_on" - android:shortcutShortLabel="@string/auto_tun_on"> + android:shortcutLongLabel="@string/start_auto" + android:shortcutShortLabel="@string/start_auto"> + android:shortcutLongLabel="@string/stop_auto" + android:shortcutShortLabel="@string/stop_auto">